moar tests
This commit is contained in:
parent
23a0ba68b9
commit
c3303cc194
14 changed files with 1947 additions and 9 deletions
|
|
@ -46,4 +46,5 @@ config :towerops, ToweropsWeb.Endpoint,
|
|||
# Use mocks for testing
|
||||
config :towerops,
|
||||
ping_module: Towerops.Monitoring.PingMock,
|
||||
poller_module: Towerops.Snmp.PollerMock
|
||||
poller_module: Towerops.Snmp.PollerMock,
|
||||
snmp_adapter: Towerops.Snmp.SnmpMock
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ defmodule Towerops.Snmp.Client do
|
|||
target = build_target(opts)
|
||||
snmp_opts = build_snmp_opts(opts)
|
||||
|
||||
case SnmpKit.get(target, oid, snmp_opts) do
|
||||
case snmp_adapter().get(target, oid, snmp_opts) do
|
||||
{:ok, value} ->
|
||||
{:ok, extract_snmp_value(value)}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ defmodule Towerops.Snmp.Client do
|
|||
# Get each OID and collect results
|
||||
results =
|
||||
Enum.map(oids, fn oid ->
|
||||
case SnmpKit.get(target, oid, snmp_opts) do
|
||||
case snmp_adapter().get(target, oid, snmp_opts) do
|
||||
{:ok, value} -> {:ok, extract_snmp_value(value)}
|
||||
error -> error
|
||||
end
|
||||
|
|
@ -116,11 +116,11 @@ defmodule Towerops.Snmp.Client do
|
|||
target = build_target(opts)
|
||||
snmp_opts = build_snmp_opts(opts)
|
||||
|
||||
case SnmpKit.walk(target, start_oid, snmp_opts) do
|
||||
case snmp_adapter().walk(target, start_oid, snmp_opts) do
|
||||
{:ok, results} when is_list(results) ->
|
||||
# snmpkit returns a list of maps, convert to OID -> value map
|
||||
walked_data =
|
||||
Map.new(results, fn %{oid: oid, value: value} -> {oid, value} end)
|
||||
Map.new(results, fn %{oid: oid, value: value} -> {oid, extract_snmp_value(value)} end)
|
||||
|
||||
{:ok, walked_data}
|
||||
|
||||
|
|
@ -168,11 +168,11 @@ defmodule Towerops.Snmp.Client do
|
|||
|
||||
snmp_opts = Keyword.put(snmp_opts, :max_repetitions, max_repetitions)
|
||||
|
||||
case SnmpKit.get_bulk(target, start_oid, snmp_opts) do
|
||||
case snmp_adapter().get_bulk(target, start_oid, snmp_opts) do
|
||||
{:ok, results} when is_list(results) ->
|
||||
# snmpkit returns a list of maps, convert to OID -> value map
|
||||
bulk_data =
|
||||
Map.new(results, fn %{oid: oid, value: value} -> {oid, value} end)
|
||||
Map.new(results, fn %{oid: oid, value: value} -> {oid, extract_snmp_value(value)} end)
|
||||
|
||||
{:ok, bulk_data}
|
||||
|
||||
|
|
@ -227,6 +227,10 @@ defmodule Towerops.Snmp.Client do
|
|||
|
||||
# Private functions
|
||||
|
||||
defp snmp_adapter do
|
||||
Application.get_env(:towerops, :snmp_adapter, SnmpKit)
|
||||
end
|
||||
|
||||
defp build_target(opts) do
|
||||
Keyword.fetch!(opts, :ip)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -134,7 +134,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
def discover_all(org_id) do
|
||||
equipment_list =
|
||||
Equipment
|
||||
|> where([e], e.organization_id == ^org_id and e.snmp_enabled == true)
|
||||
|> join(:inner, [e], s in assoc(e, :site))
|
||||
|> where([e, s], s.organization_id == ^org_id and e.snmp_enabled == true)
|
||||
|> Repo.all()
|
||||
|
||||
Logger.info("Starting SNMP discovery for #{length(equipment_list)} devices in org #{org_id}")
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ defmodule Towerops.Snmp.Profiles.Base do
|
|||
end
|
||||
|
||||
defp parse_integer(value) when is_integer(value), do: value
|
||||
defp parse_integer(""), do: nil
|
||||
defp parse_integer(value) when is_binary(value), do: String.to_integer(value)
|
||||
defp parse_integer(_), do: nil
|
||||
|
||||
|
|
|
|||
15
lib/towerops/snmp/snmp_behaviour.ex
Normal file
15
lib/towerops/snmp/snmp_behaviour.ex
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
defmodule Towerops.Snmp.SnmpBehaviour do
|
||||
@moduledoc """
|
||||
Behaviour for SNMP operations.
|
||||
Allows mocking SnmpKit in tests using Mox.
|
||||
"""
|
||||
|
||||
@type target :: String.t()
|
||||
@type oid :: String.t() | [non_neg_integer()]
|
||||
@type snmp_opts :: keyword()
|
||||
@type snmp_value :: term()
|
||||
|
||||
@callback get(target(), oid(), snmp_opts()) :: {:ok, snmp_value()} | {:error, term()}
|
||||
@callback walk(target(), oid(), snmp_opts()) :: {:ok, [map()]} | {:error, term()}
|
||||
@callback get_bulk(target(), oid(), snmp_opts()) :: {:ok, [map()]} | {:error, term()}
|
||||
end
|
||||
4
mix.exs
4
mix.exs
|
|
@ -68,11 +68,13 @@ defmodule Towerops.MixProject do
|
|||
{:bandit, "~> 1.5"},
|
||||
{:ecto_psql_extras, "~> 0.6"},
|
||||
{:mox, "~> 1.0", only: :test},
|
||||
{:stream_data, "~> 1.1", only: :test},
|
||||
{:styler, "~> 1.10", only: [:dev, :test], runtime: false},
|
||||
{:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:sobelow, "~> 0.14.1", only: [:dev, :test], runtime: false}
|
||||
{:sobelow, "~> 0.14.1", only: [:dev, :test], runtime: false},
|
||||
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
2
mix.lock
2
mix.lock
|
|
@ -28,6 +28,7 @@
|
|||
"libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
|
||||
"mix_test_watch": {:hex, :mix_test_watch, "1.4.0", "d88bcc4fbe3198871266e9d2f00cd8ae350938efbb11d3fa1da091586345adbb", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2b4693e17c8ead2ef56d4f48a0329891e8c2d0d73752c0f09272a2b17dc38d1b"},
|
||||
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
"req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"},
|
||||
"snmpkit": {:hex, :snmpkit, "1.3.19", "b09c38cea619a0a67d4fb0f3bfa3b9b749d42c400c2e7bd9446fef82f0d728a7", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: true]}], "hexpm", "b05c1f3911204c4d781234187a6f1350c369fcffe2058a85b49735b1259eb973"},
|
||||
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
|
||||
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
|
||||
"styler": {:hex, :styler, "1.10.0", "343f1f7bb19a8893c2841a9ae90665b68d2edf6cc37b964a5099e60c78815c2e", [:mix], [], "hexpm", "6a78876611869466139e63722df4cbbb56b18a842d88c19f23ca844d914ad91a"},
|
||||
"swoosh": {:hex, :swoosh, "1.19.9", "4eb2c471b8cf06adbdcaa1d57a0ad53c0ed9348ce8586a06cc491f9f0dbcb553", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "516898263a64925c31723c56bc7999a26e97b04e869707f681f4c9bca7ee1688"},
|
||||
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
|
||||
|
|
|
|||
|
|
@ -7,5 +7,16 @@ ExUnit.configure(exclude: [:integration])
|
|||
# Define mocks for testing
|
||||
Mox.defmock(Towerops.Monitoring.PingMock, for: Towerops.Monitoring.PingBehaviour)
|
||||
Mox.defmock(Towerops.Snmp.PollerMock, for: Towerops.Snmp.PollerBehaviour)
|
||||
Mox.defmock(Towerops.Snmp.SnmpMock, for: Towerops.Snmp.SnmpBehaviour)
|
||||
|
||||
# Define stub module for SNMP mock (returns errors for all calls)
|
||||
defmodule Towerops.Snmp.SnmpMockStub do
|
||||
@moduledoc false
|
||||
@behaviour Towerops.Snmp.SnmpBehaviour
|
||||
|
||||
def get(_target, _oid, _opts), do: {:error, :timeout}
|
||||
def walk(_target, _oid, _opts), do: {:error, :timeout}
|
||||
def get_bulk(_target, _oid, _opts), do: {:error, :timeout}
|
||||
end
|
||||
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Towerops.Repo, :manual)
|
||||
|
|
|
|||
395
test/towerops/snmp/client_test.exs
Normal file
395
test/towerops/snmp/client_test.exs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
defmodule Towerops.Snmp.ClientTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnitProperties
|
||||
|
||||
import Mox
|
||||
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
# Set up mocks
|
||||
setup :verify_on_exit!
|
||||
|
||||
@test_opts [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
|
||||
describe "get/2" do
|
||||
test "returns ok tuple with extracted value on success" do
|
||||
expect(SnmpMock, :get, fn "192.168.1.1", "1.3.6.1.2.1.1.3.0", _opts ->
|
||||
{:ok, {:timeticks, 12_345}}
|
||||
end)
|
||||
|
||||
assert {:ok, 12_345} = Client.get(@test_opts, "1.3.6.1.2.1.1.3.0")
|
||||
end
|
||||
|
||||
test "extracts value from different SNMP types" do
|
||||
expect(SnmpMock, :get, fn _, _, _ -> {:ok, {:octet_string, "test"}} end)
|
||||
assert {:ok, "test"} = Client.get(@test_opts, "1.3.6.1.2.1.1.1.0")
|
||||
end
|
||||
|
||||
test "returns error tuple on failure" do
|
||||
expect(SnmpMock, :get, fn _, _, _ -> {:error, :timeout} end)
|
||||
assert {:error, :timeout} = Client.get(@test_opts, "1.3.6.1.2.1.1.3.0")
|
||||
end
|
||||
|
||||
test "handles no_such_object error" do
|
||||
expect(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
assert {:error, :no_such_object} = Client.get(@test_opts, "1.3.6.1.99.99.99")
|
||||
end
|
||||
|
||||
test "handles no_such_instance error" do
|
||||
expect(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_instance} end)
|
||||
assert {:error, :no_such_instance} = Client.get(@test_opts, "1.3.6.1.2.1.1.99.0")
|
||||
end
|
||||
|
||||
test "handles end_of_mib_view error" do
|
||||
expect(SnmpMock, :get, fn _, _, _ -> {:error, :end_of_mib_view} end)
|
||||
assert {:error, :end_of_mib_view} = Client.get(@test_opts, "1.3.6.1.9.9.9.9")
|
||||
end
|
||||
|
||||
test "accepts list OID format" do
|
||||
expect(SnmpMock, :get, fn _, [1, 3, 6, 1, 2, 1, 1, 3, 0], _ ->
|
||||
{:ok, {:timeticks, 12_345}}
|
||||
end)
|
||||
|
||||
assert {:ok, 12_345} = Client.get(@test_opts, [1, 3, 6, 1, 2, 1, 1, 3, 0])
|
||||
end
|
||||
|
||||
property "extracts all SNMP value types correctly" do
|
||||
check all(
|
||||
{type, value} <-
|
||||
StreamData.one_of([
|
||||
StreamData.tuple({
|
||||
StreamData.constant(:integer),
|
||||
StreamData.integer()
|
||||
}),
|
||||
StreamData.tuple({
|
||||
StreamData.constant(:octet_string),
|
||||
StreamData.string(:alphanumeric)
|
||||
}),
|
||||
StreamData.tuple({
|
||||
StreamData.constant(:counter32),
|
||||
StreamData.integer(0..4_294_967_295)
|
||||
}),
|
||||
StreamData.tuple({
|
||||
StreamData.constant(:gauge32),
|
||||
StreamData.integer(0..4_294_967_295)
|
||||
})
|
||||
])
|
||||
) do
|
||||
expect(SnmpMock, :get, fn _, _, _ -> {:ok, {type, value}} end)
|
||||
assert {:ok, ^value} = Client.get(@test_opts, "1.3.6.1.2.1.1.1.0")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_multiple/2" do
|
||||
test "returns ok with list of values when all succeed" do
|
||||
expect(SnmpMock, :get, 2, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, {:octet_string, "Router"}}
|
||||
"1.3.6.1.2.1.1.3.0" -> {:ok, {:timeticks, 12_345}}
|
||||
end
|
||||
end)
|
||||
|
||||
oids = ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.3.0"]
|
||||
assert {:ok, ["Router", 12_345]} = Client.get_multiple(@test_opts, oids)
|
||||
end
|
||||
|
||||
test "returns error when any OID fails" do
|
||||
expect(SnmpMock, :get, 2, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, {:octet_string, "Router"}}
|
||||
"1.3.6.1.2.1.99.99.0" -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
oids = ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.99.99.0"]
|
||||
assert {:error, :partial_failure} = Client.get_multiple(@test_opts, oids)
|
||||
end
|
||||
|
||||
property "handles variable number of OIDs" do
|
||||
check all(count <- StreamData.integer(1..10)) do
|
||||
expect(SnmpMock, :get, count, fn _, _, _ ->
|
||||
{:ok, {:integer, 42}}
|
||||
end)
|
||||
|
||||
oids = Enum.map(1..count, fn i -> "1.3.6.1.2.1.1.#{i}.0" end)
|
||||
assert {:ok, values} = Client.get_multiple(@test_opts, oids)
|
||||
assert length(values) == count
|
||||
assert Enum.all?(values, &(&1 == 42))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "walk/2" do
|
||||
test "returns map of OID to value on success" do
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.2", _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.2.1", value: "eth0"},
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.2.2", value: "eth1"}
|
||||
]}
|
||||
end)
|
||||
|
||||
assert {:ok, results} = Client.walk(@test_opts, "1.3.6.1.2.1.2.2.1.2")
|
||||
assert is_map(results)
|
||||
assert results["1.3.6.1.2.1.2.2.1.2.1"] == "eth0"
|
||||
assert results["1.3.6.1.2.1.2.2.1.2.2"] == "eth1"
|
||||
end
|
||||
|
||||
test "returns error tuple on failure" do
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)
|
||||
assert {:error, :timeout} = Client.walk(@test_opts, "1.3.6.1.2.1.2.2.1.2")
|
||||
end
|
||||
|
||||
test "handles empty walk results" do
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
|
||||
assert {:ok, results} = Client.walk(@test_opts, "1.3.6.1.2.1.99.99")
|
||||
assert results == %{}
|
||||
end
|
||||
|
||||
property "converts list of results to map" do
|
||||
check all(count <- StreamData.integer(1..20)) do
|
||||
results =
|
||||
Enum.map(1..count, fn i ->
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.2.#{i}", value: "eth#{i}"}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, _, _ -> {:ok, results} end)
|
||||
assert {:ok, walked} = Client.walk(@test_opts, "1.3.6.1.2.1.2.2.1.2")
|
||||
assert map_size(walked) == count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_bulk/3" do
|
||||
test "returns map of OID to value on success" do
|
||||
expect(SnmpMock, :get_bulk, fn _, "1.3.6.1.2.1.2.2.1.10", opts ->
|
||||
assert opts[:max_repetitions] == 10
|
||||
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.10.1", value: 1000},
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.10.2", value: 2000}
|
||||
]}
|
||||
end)
|
||||
|
||||
assert {:ok, results} = Client.get_bulk(@test_opts, "1.3.6.1.2.1.2.2.1.10", max_repetitions: 10)
|
||||
assert is_map(results)
|
||||
assert results["1.3.6.1.2.1.2.2.1.10.1"] == 1000
|
||||
end
|
||||
|
||||
test "uses default max_repetitions of 10" do
|
||||
expect(SnmpMock, :get_bulk, fn _, _, opts ->
|
||||
assert opts[:max_repetitions] == 10
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
Client.get_bulk(@test_opts, "1.3.6.1.2.1.2.2.1.10")
|
||||
end
|
||||
|
||||
test "uses custom max_repetitions" do
|
||||
expect(SnmpMock, :get_bulk, fn _, _, opts ->
|
||||
assert opts[:max_repetitions] == 50
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
Client.get_bulk(@test_opts, "1.3.6.1.2.1.2.2.1.10", max_repetitions: 50)
|
||||
end
|
||||
|
||||
test "returns error tuple on failure" do
|
||||
expect(SnmpMock, :get_bulk, fn _, _, _ -> {:error, :timeout} end)
|
||||
assert {:error, :timeout} = Client.get_bulk(@test_opts, "1.3.6.1.2.1.2.2.1.10")
|
||||
end
|
||||
end
|
||||
|
||||
describe "test_connection/1" do
|
||||
test "returns success when sysUpTime is retrieved" do
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:ok, {:timeticks, 12_345}}
|
||||
end)
|
||||
|
||||
assert {:ok, "Connection successful"} = Client.test_connection(@test_opts)
|
||||
end
|
||||
|
||||
test "returns error when connection fails" do
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, :timeout} = Client.test_connection(@test_opts)
|
||||
end
|
||||
|
||||
test "returns error when host is unreachable" do
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:error, :network_unreachable}
|
||||
end)
|
||||
|
||||
assert {:error, :network_unreachable} = Client.test_connection(@test_opts)
|
||||
end
|
||||
end
|
||||
|
||||
describe "connection options" do
|
||||
property "valid connection options include all required fields" do
|
||||
check all(
|
||||
ip <- StreamData.string(:alphanumeric, min_length: 7, max_length: 15),
|
||||
community <- StreamData.string(:alphanumeric, min_length: 1, max_length: 32),
|
||||
version <- StreamData.member_of(["1", "2c"]),
|
||||
port <- StreamData.integer(1..65_535),
|
||||
timeout <- StreamData.integer(1000..30_000)
|
||||
) do
|
||||
opts = [
|
||||
ip: ip,
|
||||
community: community,
|
||||
version: version,
|
||||
port: port,
|
||||
timeout: timeout
|
||||
]
|
||||
|
||||
# Should have all required fields
|
||||
assert Keyword.has_key?(opts, :ip)
|
||||
assert Keyword.has_key?(opts, :community)
|
||||
assert Keyword.has_key?(opts, :version)
|
||||
|
||||
# Optional fields with valid values
|
||||
assert opts[:port] > 0 and opts[:port] <= 65_535
|
||||
assert opts[:timeout] >= 1000
|
||||
end
|
||||
end
|
||||
|
||||
test "minimal valid options" do
|
||||
opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
|
||||
assert Keyword.has_key?(opts, :ip)
|
||||
assert Keyword.has_key?(opts, :community)
|
||||
assert Keyword.has_key?(opts, :version)
|
||||
end
|
||||
end
|
||||
|
||||
describe "OID formats" do
|
||||
property "accepts string OIDs" do
|
||||
check all(oid_parts <- StreamData.list_of(StreamData.positive_integer(), min_length: 3, max_length: 10)) do
|
||||
oid = Enum.join(oid_parts, ".")
|
||||
|
||||
# Valid OID format
|
||||
assert oid =~ ~r/^[\d.]+$/
|
||||
assert is_binary(oid)
|
||||
end
|
||||
end
|
||||
|
||||
property "accepts list OIDs" do
|
||||
check all(oid_list <- StreamData.list_of(StreamData.non_negative_integer(), min_length: 3, max_length: 10)) do
|
||||
# All elements are integers
|
||||
assert Enum.all?(oid_list, &is_integer/1)
|
||||
|
||||
# All elements are non-negative
|
||||
assert Enum.all?(oid_list, &(&1 >= 0))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "IP address formats" do
|
||||
property "IP addresses are strings" do
|
||||
check all(octets <- StreamData.list_of(StreamData.integer(0..255), length: 4)) do
|
||||
ip = Enum.join(octets, ".")
|
||||
opts = [ip: ip, community: "public", version: "2c"]
|
||||
|
||||
assert is_binary(opts[:ip])
|
||||
end
|
||||
end
|
||||
|
||||
test "IPv4 format" do
|
||||
valid_ips = [
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
"8.8.8.8",
|
||||
"127.0.0.1"
|
||||
]
|
||||
|
||||
for ip <- valid_ips do
|
||||
opts = [ip: ip, community: "public", version: "2c"]
|
||||
assert opts[:ip] == ip
|
||||
end
|
||||
end
|
||||
|
||||
test "hostname format" do
|
||||
valid_hosts = [
|
||||
"router.example.com",
|
||||
"switch-01",
|
||||
"device123"
|
||||
]
|
||||
|
||||
for host <- valid_hosts do
|
||||
opts = [ip: host, community: "public", version: "2c"]
|
||||
assert opts[:ip] == host
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "SNMP version handling" do
|
||||
test "supports SNMPv1" do
|
||||
opts = [ip: "192.168.1.1", community: "public", version: "1"]
|
||||
assert opts[:version] == "1"
|
||||
end
|
||||
|
||||
test "supports SNMPv2c" do
|
||||
opts = [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
assert opts[:version] == "2c"
|
||||
end
|
||||
|
||||
property "version is always v1 or v2c" do
|
||||
check all(version <- StreamData.member_of(["1", "2c", :v1, :v2c])) do
|
||||
opts = [ip: "192.168.1.1", community: "public", version: version]
|
||||
|
||||
normalized_version =
|
||||
case opts[:version] do
|
||||
"1" -> :v1
|
||||
"2c" -> :v2c
|
||||
:v1 -> :v1
|
||||
:v2c -> :v2c
|
||||
end
|
||||
|
||||
assert normalized_version in [:v1, :v2c]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "community strings" do
|
||||
property "community strings can be any non-empty string" do
|
||||
check all(community <- StreamData.string(:alphanumeric, min_length: 1, max_length: 64)) do
|
||||
opts = [ip: "192.168.1.1", community: community, version: "2c"]
|
||||
|
||||
assert is_binary(opts[:community])
|
||||
assert String.length(opts[:community]) > 0
|
||||
end
|
||||
end
|
||||
|
||||
test "common community strings" do
|
||||
common_communities = ["public", "private", "community", "snmp"]
|
||||
|
||||
for community <- common_communities do
|
||||
opts = [ip: "192.168.1.1", community: community, version: "2c"]
|
||||
assert opts[:community] == community
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "return value formats" do
|
||||
property "error reasons are atoms" do
|
||||
check all(
|
||||
reason <-
|
||||
StreamData.member_of([
|
||||
:timeout,
|
||||
:no_such_object,
|
||||
:no_such_instance,
|
||||
:end_of_mib_view,
|
||||
:network_unreachable
|
||||
])
|
||||
) do
|
||||
expect(SnmpMock, :get, fn _, _, _ -> {:error, reason} end)
|
||||
assert {:error, ^reason} = Client.get(@test_opts, "1.3.6.1.2.1.1.1.0")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
479
test/towerops/snmp/discovery_test.exs
Normal file
479
test/towerops/snmp/discovery_test.exs
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
defmodule Towerops.Snmp.DiscoveryTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Mox
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Snmp.Device
|
||||
alias Towerops.Snmp.Discovery
|
||||
alias Towerops.Snmp.Interface
|
||||
alias Towerops.Snmp.Sensor
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
||||
{:ok, site} =
|
||||
Towerops.Sites.create_site(%{
|
||||
name: "Test Site",
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
%{organization: organization, site: site}
|
||||
end
|
||||
|
||||
describe "discover_equipment/1" do
|
||||
test "returns error when SNMP is not enabled", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Router 1",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
snmp_enabled: false
|
||||
})
|
||||
|
||||
assert {:error, :snmp_not_enabled} = Discovery.discover_equipment(equipment)
|
||||
end
|
||||
|
||||
test "successfully discovers MikroTik device", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "MikroTik Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
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, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "RouterOS RB750"}}
|
||||
|
||||
"1.3.6.1.2.1.1.2.0" ->
|
||||
{:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 14_988, 1]}}
|
||||
|
||||
"1.3.6.1.2.1.1.3.0" ->
|
||||
{:ok, {:timeticks, 12_345}}
|
||||
|
||||
"1.3.6.1.2.1.1.4.0" ->
|
||||
{:ok, {:octet_string, "admin@example.com"}}
|
||||
|
||||
"1.3.6.1.2.1.1.5.0" ->
|
||||
{:ok, {:octet_string, "router1"}}
|
||||
|
||||
"1.3.6.1.2.1.1.6.0" ->
|
||||
{:ok, {:octet_string, "Data Center"}}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
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)
|
||||
|
||||
# Mock MikroTik health sensor discovery (15 sensors: voltages, temps, power, fans)
|
||||
# All MikroTik-specific OIDs will return no_such_object since device doesn't have them
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ ->
|
||||
{:error, :no_such_object}
|
||||
end)
|
||||
|
||||
# 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
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
if String.starts_with?(oid, "1.3.6.1.2.1.99") do
|
||||
{:ok, []}
|
||||
else
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, device} = Discovery.discover_equipment(equipment)
|
||||
|
||||
assert device.manufacturer == "MikroTik"
|
||||
assert device.model == "RB750"
|
||||
assert device.sys_name == "router1"
|
||||
|
||||
# Verify interfaces were created
|
||||
interfaces = Repo.all(from i in Interface, where: i.snmp_device_id == ^device.id)
|
||||
assert length(interfaces) == 2
|
||||
|
||||
interface1 = Enum.find(interfaces, &(&1.if_index == 1))
|
||||
assert interface1.if_descr == "ether1"
|
||||
assert interface1.if_name == "ether1"
|
||||
assert interface1.if_alias == "WAN"
|
||||
end
|
||||
|
||||
test "successfully discovers Cisco device", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Cisco Switch",
|
||||
ip_address: "192.168.1.2",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
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, _ ->
|
||||
case oid do
|
||||
"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"}}
|
||||
|
||||
"1.3.6.1.2.1.1.5.0" ->
|
||||
{:ok, {:octet_string, "switch1"}}
|
||||
|
||||
"1.3.6.1.2.1.1.6.0" ->
|
||||
{:ok, {:octet_string, "Building A"}}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock interface discovery - no interfaces for simplicity
|
||||
# Walk called 3 times: interfaces (1), cisco sensors (1), base sensors fallback (1)
|
||||
expect(SnmpMock, :walk, 3, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert {:ok, device} = Discovery.discover_equipment(equipment)
|
||||
|
||||
assert device.manufacturer == "Cisco"
|
||||
assert device.model == "C2960"
|
||||
assert device.sys_name == "switch1"
|
||||
end
|
||||
|
||||
test "handles connection failure", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Unreachable Device",
|
||||
ip_address: "192.168.1.99",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
# Mock failed connection test
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, :timeout} = Discovery.discover_equipment(equipment)
|
||||
end
|
||||
|
||||
test "handles partial discovery failure gracefully", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Partial Device",
|
||||
ip_address: "192.168.1.3",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
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, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "Generic Device"}}
|
||||
|
||||
_ ->
|
||||
{:ok, {:octet_string, ""}}
|
||||
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
|
||||
expect(SnmpMock, :walk, fn _, oid, _ ->
|
||||
if String.starts_with?(oid, "1.3.6.1.2.1.99") do
|
||||
{:error, :timeout}
|
||||
else
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Should succeed even with failed interface/sensor discovery
|
||||
assert {:ok, device} = Discovery.discover_equipment(equipment)
|
||||
|
||||
assert device.manufacturer == "Unknown"
|
||||
|
||||
# Verify no interfaces or sensors were created
|
||||
interfaces = Repo.all(from i in Interface, where: i.snmp_device_id == ^device.id)
|
||||
sensors = Repo.all(from s in Sensor, where: s.snmp_device_id == ^device.id)
|
||||
|
||||
assert interfaces == []
|
||||
assert sensors == []
|
||||
end
|
||||
|
||||
test "updates existing device on re-discovery", %{site: site} do
|
||||
{:ok, equipment} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Existing Device",
|
||||
ip_address: "192.168.1.4",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
# Create initial device
|
||||
{:ok, initial_device} =
|
||||
%Device{}
|
||||
|> Device.changeset(%{
|
||||
equipment_id: equipment.id,
|
||||
manufacturer: "Old Manufacturer",
|
||||
model: "Old Model",
|
||||
sys_name: "old-name"
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
# Mock connection and system info for re-discovery
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:ok, {:timeticks, 12_345}}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "Linux server 5.4.0"}}
|
||||
|
||||
"1.3.6.1.2.1.1.5.0" ->
|
||||
{:ok, {:octet_string, "updated-name"}}
|
||||
|
||||
_ ->
|
||||
{:ok, {:octet_string, ""}}
|
||||
end
|
||||
end)
|
||||
|
||||
# NetSnmp also tries UCD sensors (load, memory, disk) which make additional get calls
|
||||
stub(SnmpMock, :get, fn _, _, _ ->
|
||||
{:error, :no_such_object}
|
||||
end)
|
||||
|
||||
# NetSnmp profile makes many walk calls for sensors
|
||||
# Interfaces (1), LM sensors (6), UCD sensors (3), ENTITY-SENSOR-MIB fallback (1)
|
||||
stub(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert {:ok, updated_device} = Discovery.discover_equipment(equipment)
|
||||
|
||||
# Should be same device, updated
|
||||
assert updated_device.id == initial_device.id
|
||||
assert updated_device.manufacturer == "Linux"
|
||||
assert updated_device.model == "Linux"
|
||||
assert updated_device.sys_name == "updated-name"
|
||||
end
|
||||
end
|
||||
|
||||
describe "discover_all/1" do
|
||||
test "discovers multiple devices concurrently", %{organization: organization, site: site} do
|
||||
# Create multiple SNMP-enabled equipment
|
||||
{:ok, equipment1} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Device 1",
|
||||
ip_address: "192.168.1.10",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
{:ok, equipment2} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Device 2",
|
||||
ip_address: "192.168.1.11",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
{:ok, _equipment3} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Device 3 - No SNMP",
|
||||
ip_address: "192.168.1.12",
|
||||
site_id: site.id,
|
||||
snmp_enabled: false
|
||||
})
|
||||
|
||||
# Mock successful discovery for device1
|
||||
allow(SnmpMock, self(), fn -> :ok end)
|
||||
|
||||
stub(SnmpMock, :get, fn _target, oid, _opts ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.3.0" ->
|
||||
{:ok, {:timeticks, 12_345}}
|
||||
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "Test Device"}}
|
||||
|
||||
_ ->
|
||||
{:ok, {:octet_string, ""}}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert {:ok, summary} = Discovery.discover_all(organization.id)
|
||||
|
||||
# Should only discover SNMP-enabled devices
|
||||
assert summary.success == 2
|
||||
assert summary.failed == 0
|
||||
|
||||
# Verify devices were created
|
||||
assert Repo.get_by(Device, equipment_id: equipment1.id)
|
||||
assert Repo.get_by(Device, equipment_id: equipment2.id)
|
||||
end
|
||||
|
||||
test "handles mixed success and failure", %{organization: organization, site: site} do
|
||||
{:ok, equipment1} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Good Device",
|
||||
ip_address: "192.168.1.20",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "public",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
{:ok, _equipment2} =
|
||||
Equipment.create_equipment(%{
|
||||
name: "Bad Device",
|
||||
ip_address: "192.168.1.21",
|
||||
site_id: site.id,
|
||||
snmp_enabled: true,
|
||||
snmp_version: "2c",
|
||||
snmp_community: "wrong",
|
||||
snmp_port: 161
|
||||
})
|
||||
|
||||
allow(SnmpMock, self(), fn -> :ok end)
|
||||
|
||||
stub(SnmpMock, :get, fn target, oid, _opts ->
|
||||
# Fail for .21, succeed for .20
|
||||
if target == "192.168.1.21" do
|
||||
{:error, :auth_failure}
|
||||
else
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.3.0" -> {:ok, {:timeticks, 12_345}}
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, {:octet_string, "Good Device"}}
|
||||
_ -> {:ok, {:octet_string, ""}}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :walk, fn target, _, _ ->
|
||||
if target == "192.168.1.21" do
|
||||
{:error, :auth_failure}
|
||||
else
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, summary} = Discovery.discover_all(organization.id)
|
||||
|
||||
assert summary.success == 1
|
||||
assert summary.failed == 1
|
||||
assert :auth_failure in summary.errors
|
||||
|
||||
# Verify only good device was created
|
||||
assert Repo.get_by(Device, equipment_id: equipment1.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
169
test/towerops/snmp/poller_test.exs
Normal file
169
test/towerops/snmp/poller_test.exs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
defmodule Towerops.Snmp.PollerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Mox
|
||||
|
||||
alias Towerops.Snmp.Poller
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "check_device/1" do
|
||||
test "returns response time on successful check" do
|
||||
client_opts = [
|
||||
ip: "192.168.1.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:ok, {:timeticks, 12_345_678}}
|
||||
end)
|
||||
|
||||
assert {:ok, response_time} = Poller.check_device(client_opts)
|
||||
assert is_integer(response_time)
|
||||
assert response_time >= 0
|
||||
end
|
||||
|
||||
test "returns error on timeout" do
|
||||
client_opts = [
|
||||
ip: "192.168.1.99",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
timeout: 1000
|
||||
]
|
||||
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, :timeout} = Poller.check_device(client_opts)
|
||||
end
|
||||
|
||||
test "returns error on authentication failure" do
|
||||
client_opts = [
|
||||
ip: "192.168.1.1",
|
||||
community: "wrong_community",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:error, :auth_failure}
|
||||
end)
|
||||
|
||||
assert {:error, :auth_failure} = Poller.check_device(client_opts)
|
||||
end
|
||||
|
||||
test "returns error on network unreachable" do
|
||||
client_opts = [
|
||||
ip: "10.255.255.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:error, :network_unreachable}
|
||||
end)
|
||||
|
||||
assert {:error, :network_unreachable} = Poller.check_device(client_opts)
|
||||
end
|
||||
|
||||
test "measures response time accurately" do
|
||||
client_opts = [
|
||||
ip: "192.168.1.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
||||
# Simulate a delayed response
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
Process.sleep(10)
|
||||
{:ok, {:timeticks, 999}}
|
||||
end)
|
||||
|
||||
assert {:ok, response_time} = Poller.check_device(client_opts)
|
||||
# Response time should be at least 10ms due to the sleep
|
||||
assert response_time >= 10
|
||||
end
|
||||
end
|
||||
|
||||
describe "build_client_opts/1" do
|
||||
test "builds client options from equipment map" do
|
||||
equipment = %{
|
||||
ip_address: "192.168.1.100",
|
||||
snmp_community: "private",
|
||||
snmp_version: "2c",
|
||||
snmp_port: 161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
|
||||
assert client_opts[:ip] == "192.168.1.100"
|
||||
assert client_opts[:community] == "private"
|
||||
assert client_opts[:version] == "2c"
|
||||
assert client_opts[:port] == 161
|
||||
assert client_opts[:timeout] == 5000
|
||||
end
|
||||
|
||||
test "uses default port 161 when snmp_port is nil" do
|
||||
equipment = %{
|
||||
ip_address: "192.168.1.100",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
snmp_port: nil
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
|
||||
assert client_opts[:port] == 161
|
||||
end
|
||||
|
||||
test "handles custom SNMP port" do
|
||||
equipment = %{
|
||||
ip_address: "192.168.1.100",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
snmp_port: 1161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
|
||||
assert client_opts[:port] == 1161
|
||||
end
|
||||
|
||||
test "builds options for SNMPv1" do
|
||||
equipment = %{
|
||||
ip_address: "10.0.0.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "1",
|
||||
snmp_port: 161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
|
||||
assert client_opts[:version] == "1"
|
||||
end
|
||||
|
||||
test "includes standard timeout of 5 seconds" do
|
||||
equipment = %{
|
||||
ip_address: "192.168.1.1",
|
||||
snmp_community: "public",
|
||||
snmp_version: "2c",
|
||||
snmp_port: 161
|
||||
}
|
||||
|
||||
client_opts = Poller.build_client_opts(equipment)
|
||||
|
||||
assert client_opts[:timeout] == 5000
|
||||
end
|
||||
end
|
||||
end
|
||||
365
test/towerops/snmp/profiles/base_test.exs
Normal file
365
test/towerops/snmp/profiles/base_test.exs
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
defmodule Towerops.Snmp.Profiles.BaseTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Mox
|
||||
|
||||
alias Towerops.Snmp.Profiles.Base
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@client_opts [
|
||||
ip: "192.168.1.1",
|
||||
community: "public",
|
||||
version: "2c",
|
||||
port: 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
||||
describe "discover_system_info/1" do
|
||||
test "successfully discovers system information" do
|
||||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "Linux server 5.4.0-42-generic"}}
|
||||
|
||||
"1.3.6.1.2.1.1.2.0" ->
|
||||
{:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 8072, 3, 2, 10]}}
|
||||
|
||||
"1.3.6.1.2.1.1.3.0" ->
|
||||
{:ok, {:timeticks, 123_456_789}}
|
||||
|
||||
"1.3.6.1.2.1.1.4.0" ->
|
||||
{:ok, {:octet_string, "admin@example.com"}}
|
||||
|
||||
"1.3.6.1.2.1.1.5.0" ->
|
||||
{:ok, {:octet_string, "webserver01"}}
|
||||
|
||||
"1.3.6.1.2.1.1.6.0" ->
|
||||
{:ok, {:octet_string, "DC1 Rack 42"}}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, system_info} = Base.discover_system_info(@client_opts)
|
||||
|
||||
assert system_info.sys_descr == "Linux server 5.4.0-42-generic"
|
||||
assert system_info.sys_object_id == "1.3.6.1.4.1.8072.3.2.10"
|
||||
assert system_info.sys_name == "webserver01"
|
||||
assert system_info.sys_uptime == 123_456_789
|
||||
assert system_info.sys_contact == "admin@example.com"
|
||||
assert system_info.sys_location == "DC1 Rack 42"
|
||||
end
|
||||
|
||||
test "handles partial failure gracefully" do
|
||||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "Test Device"}}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Should fail because get_multiple returns error on any failure
|
||||
assert {:error, :partial_failure} = Base.discover_system_info(@client_opts)
|
||||
end
|
||||
|
||||
test "handles timeout errors" do
|
||||
expect(SnmpMock, :get, 6, fn _, _, _ ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, :partial_failure} = Base.discover_system_info(@client_opts)
|
||||
end
|
||||
end
|
||||
|
||||
describe "discover_interfaces/1" do
|
||||
test "successfully discovers multiple interfaces" do
|
||||
# Mock ifIndex walk
|
||||
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}},
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.1.3", value: {:integer, 3}}
|
||||
]}
|
||||
end)
|
||||
|
||||
# Mock interface data for each interface (8 OIDs per interface × 3 interfaces = 24 calls)
|
||||
expect(SnmpMock, :get, 24, 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, "eth0"}}
|
||||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||||
second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
|
||||
second_to_last == "6" -> {:ok, {:octet_string, <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01>>}}
|
||||
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, "eth0"}}
|
||||
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, "eth1"}}
|
||||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||||
second_to_last == "5" -> {:ok, {:gauge32, 100_000_000}}
|
||||
second_to_last == "6" -> {:ok, {:octet_string, <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02>>}}
|
||||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "8" -> {:ok, {:integer, 2}}
|
||||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "eth1"}}
|
||||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "LAN"}}
|
||||
true -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
String.ends_with?(oid, ".3") ->
|
||||
parts = String.split(oid, ".")
|
||||
second_to_last = Enum.at(parts, -2)
|
||||
|
||||
cond do
|
||||
second_to_last == "2" -> {:ok, {:octet_string, "lo"}}
|
||||
second_to_last == "3" -> {:ok, {:integer, 24}}
|
||||
second_to_last == "5" -> {:ok, {:gauge32, 10_000_000}}
|
||||
second_to_last == "6" -> {:ok, {:octet_string, <<>>}}
|
||||
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, "lo"}}
|
||||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "Loopback"}}
|
||||
true -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, interfaces} = Base.discover_interfaces(@client_opts)
|
||||
|
||||
assert length(interfaces) == 3
|
||||
|
||||
eth0 = Enum.find(interfaces, &(&1.if_index == 1))
|
||||
assert eth0.if_descr == "eth0"
|
||||
assert eth0.if_name == "eth0"
|
||||
assert eth0.if_alias == "WAN"
|
||||
assert eth0.if_type == 6
|
||||
assert eth0.if_speed == 1_000_000_000
|
||||
assert eth0.if_phys_address == "aa:bb:cc:dd:ee:01"
|
||||
assert eth0.if_admin_status == "up"
|
||||
assert eth0.if_oper_status == "up"
|
||||
|
||||
eth1 = Enum.find(interfaces, &(&1.if_index == 2))
|
||||
assert eth1.if_descr == "eth1"
|
||||
assert eth1.if_oper_status == "down"
|
||||
|
||||
lo = Enum.find(interfaces, &(&1.if_index == 3))
|
||||
assert lo.if_descr == "lo"
|
||||
assert lo.if_phys_address == nil
|
||||
end
|
||||
|
||||
test "handles empty interface list" do
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert {:ok, interfaces} = Base.discover_interfaces(@client_opts)
|
||||
assert interfaces == []
|
||||
end
|
||||
|
||||
test "handles walk failure" do
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
assert {:error, :timeout} = Base.discover_interfaces(@client_opts)
|
||||
end
|
||||
end
|
||||
|
||||
describe "discover_sensors/1" do
|
||||
test "discovers sensors from ENTITY-SENSOR-MIB" do
|
||||
# Mock sensor type walk
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.1", value: {:integer, 8}},
|
||||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.2", value: {:integer, 3}}
|
||||
]}
|
||||
end)
|
||||
|
||||
# Mock sensor data for each sensor (4 OIDs per sensor × 2 sensors = 8 calls)
|
||||
expect(SnmpMock, :get, 8, fn _, oid, _ ->
|
||||
cond do
|
||||
String.ends_with?(oid, ".1") ->
|
||||
case oid |> String.split(".") |> Enum.at(-2) do
|
||||
"1" -> {:ok, {:integer, 8}}
|
||||
"2" -> {:ok, {:integer, 0}}
|
||||
"4" -> {:ok, {:integer, 45}}
|
||||
"5" -> {:ok, {:integer, 1}}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
String.ends_with?(oid, ".2") ->
|
||||
case oid |> String.split(".") |> Enum.at(-2) do
|
||||
"1" -> {:ok, {:integer, 3}}
|
||||
"2" -> {:ok, {:integer, -3}}
|
||||
"4" -> {:ok, {:integer, 12_000}}
|
||||
"5" -> {:ok, {:integer, 1}}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||||
|
||||
assert length(sensors) == 2
|
||||
|
||||
temp_sensor = Enum.find(sensors, &(&1.sensor_index == "1"))
|
||||
assert temp_sensor.sensor_type == "celsius"
|
||||
assert temp_sensor.sensor_unit == "°C"
|
||||
assert temp_sensor.sensor_divisor == 1
|
||||
assert temp_sensor.last_value == 45.0
|
||||
assert temp_sensor.status == "ok"
|
||||
|
||||
voltage_sensor = Enum.find(sensors, &(&1.sensor_index == "2"))
|
||||
assert voltage_sensor.sensor_type == "volts"
|
||||
assert voltage_sensor.sensor_unit == "V"
|
||||
assert voltage_sensor.sensor_divisor == 1_000
|
||||
assert voltage_sensor.last_value == 12_000.0
|
||||
end
|
||||
|
||||
test "returns empty list when ENTITY-SENSOR-MIB not supported" do
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||||
assert sensors == []
|
||||
end
|
||||
|
||||
test "returns empty list on walk error" do
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
|
||||
{:error, :no_such_object}
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||||
assert sensors == []
|
||||
end
|
||||
|
||||
test "filters out sensors with missing data" do
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.1", value: {:integer, 8}},
|
||||
%{oid: "1.3.6.1.2.1.99.1.1.1.1.2", value: {:integer, 8}}
|
||||
]}
|
||||
end)
|
||||
|
||||
# First sensor succeeds, second fails
|
||||
expect(SnmpMock, :get, 8, fn _, oid, _ ->
|
||||
if String.ends_with?(oid, ".1") do
|
||||
case oid |> String.split(".") |> Enum.at(-2) do
|
||||
"1" -> {:ok, {:integer, 8}}
|
||||
"2" -> {:ok, {:integer, 0}}
|
||||
"4" -> {:ok, {:integer, 42}}
|
||||
"5" -> {:ok, {:integer, 1}}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
else
|
||||
{:error, :timeout}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Base.discover_sensors(@client_opts)
|
||||
|
||||
# Should only have the successful sensor
|
||||
assert length(sensors) == 1
|
||||
assert hd(sensors).sensor_index == "1"
|
||||
end
|
||||
end
|
||||
|
||||
describe "identify_device/1" do
|
||||
test "identifies Cisco device from sysDescr" do
|
||||
system_info = %{
|
||||
sys_descr: "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.0(2)SE"
|
||||
}
|
||||
|
||||
device_info = Base.identify_device(system_info)
|
||||
|
||||
assert device_info.manufacturer == "Cisco"
|
||||
assert device_info.model == "C2960"
|
||||
end
|
||||
|
||||
test "identifies Linux server from sysDescr" do
|
||||
system_info = %{
|
||||
sys_descr: "Linux server 5.4.0-42-generic #46-Ubuntu SMP"
|
||||
}
|
||||
|
||||
device_info = Base.identify_device(system_info)
|
||||
|
||||
assert device_info.manufacturer == "Linux"
|
||||
assert device_info.model == "Server"
|
||||
end
|
||||
|
||||
test "identifies Windows server from sysDescr" do
|
||||
system_info = %{
|
||||
sys_descr: "Windows Server 2019 Standard"
|
||||
}
|
||||
|
||||
device_info = Base.identify_device(system_info)
|
||||
|
||||
assert device_info.manufacturer == "Microsoft"
|
||||
assert device_info.model == "Windows Server"
|
||||
end
|
||||
|
||||
test "falls back to unknown for unrecognized devices" do
|
||||
system_info = %{
|
||||
sys_descr: "Some Unknown Device"
|
||||
}
|
||||
|
||||
device_info = Base.identify_device(system_info)
|
||||
|
||||
assert device_info.manufacturer == "Unknown"
|
||||
assert device_info.model == "Generic Device"
|
||||
end
|
||||
|
||||
test "extracts Cisco model from complex sysDescr" do
|
||||
system_info = %{
|
||||
sys_descr: "Cisco IOS Software, WS-C3750X Software (cat3k_caa-universalk9-M), Version 15.2(4)E1"
|
||||
}
|
||||
|
||||
device_info = Base.identify_device(system_info)
|
||||
|
||||
assert device_info.manufacturer == "Cisco"
|
||||
assert device_info.model == "WS-C3750X"
|
||||
end
|
||||
|
||||
test "preserves other system_info fields" do
|
||||
system_info = %{
|
||||
sys_descr: "Linux server",
|
||||
sys_name: "webserver01",
|
||||
sys_contact: "admin@example.com",
|
||||
sys_location: "DC1"
|
||||
}
|
||||
|
||||
device_info = Base.identify_device(system_info)
|
||||
|
||||
assert device_info.sys_name == "webserver01"
|
||||
assert device_info.sys_contact == "admin@example.com"
|
||||
assert device_info.sys_location == "DC1"
|
||||
end
|
||||
end
|
||||
end
|
||||
477
test/towerops/snmp/profiles/mikrotik_test.exs
Normal file
477
test/towerops/snmp/profiles/mikrotik_test.exs
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnitProperties
|
||||
|
||||
import Mox
|
||||
|
||||
alias Towerops.Snmp.Profiles.Mikrotik
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@test_opts [ip: "192.168.1.1", community: "public", version: "2c"]
|
||||
|
||||
describe "discover_system_info/1" do
|
||||
test "merges base system info with MikroTik-specific info" do
|
||||
# Mock Base.discover_system_info (via Client.get_multiple for system OIDs)
|
||||
base_system_oids = [
|
||||
"1.3.6.1.2.1.1.1.0",
|
||||
"1.3.6.1.2.1.1.2.0",
|
||||
"1.3.6.1.2.1.1.3.0",
|
||||
"1.3.6.1.2.1.1.4.0",
|
||||
"1.3.6.1.2.1.1.5.0",
|
||||
"1.3.6.1.2.1.1.6.0"
|
||||
]
|
||||
|
||||
expect(SnmpMock, :get, length(base_system_oids), fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "RouterOS RB5009UG+S+ (stable) 7.13.2"}}
|
||||
|
||||
"1.3.6.1.2.1.1.2.0" ->
|
||||
{:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 14_988, 1]}}
|
||||
|
||||
"1.3.6.1.2.1.1.3.0" ->
|
||||
{:ok, {:timeticks, 123_456_789}}
|
||||
|
||||
"1.3.6.1.2.1.1.4.0" ->
|
||||
{:ok, {:octet_string, "admin@example.com"}}
|
||||
|
||||
"1.3.6.1.2.1.1.5.0" ->
|
||||
{:ok, {:octet_string, "rb5009-main"}}
|
||||
|
||||
"1.3.6.1.2.1.1.6.0" ->
|
||||
{:ok, {:octet_string, "Data Center"}}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock MikroTik-specific OIDs
|
||||
mikrotik_oids = [
|
||||
"1.3.6.1.4.1.14988.1.1.7.3.0",
|
||||
"1.3.6.1.4.1.14988.1.1.7.4.0",
|
||||
"1.3.6.1.4.1.14988.1.1.7.9.0",
|
||||
"1.3.6.1.4.1.14988.1.1.7.8.0",
|
||||
"1.3.6.1.4.1.14988.1.1.4.4.0"
|
||||
]
|
||||
|
||||
expect(SnmpMock, :get, length(mikrotik_oids), fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.4.1.14988.1.1.7.3.0" -> {:ok, {:octet_string, "ABC123DEF456"}}
|
||||
"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, "RB5009UG+S+"}}
|
||||
"1.3.6.1.4.1.14988.1.1.7.8.0" -> {:ok, {:octet_string, "RB5009"}}
|
||||
"1.3.6.1.4.1.14988.1.1.4.4.0" -> {:ok, {:octet_string, "level6"}}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, info} = Mikrotik.discover_system_info(@test_opts)
|
||||
|
||||
# Check base info
|
||||
assert info.sys_descr == "RouterOS RB5009UG+S+ (stable) 7.13.2"
|
||||
assert info.sys_name == "rb5009-main"
|
||||
assert info.sys_location == "Data Center"
|
||||
|
||||
# Check MikroTik-specific info
|
||||
assert info.serial_number == "ABC123DEF456"
|
||||
assert info.firmware_version == "7.13.2"
|
||||
assert info.board_name == "RB5009UG+S+"
|
||||
end
|
||||
|
||||
test "handles missing MikroTik-specific OIDs gracefully" do
|
||||
# Mock Base system info
|
||||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, {:octet_string, "RouterOS Device"}}
|
||||
"1.3.6.1.2.1.1.2.0" -> {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 14_988]}}
|
||||
"1.3.6.1.2.1.1.3.0" -> {:ok, {:timeticks, 12_345}}
|
||||
"1.3.6.1.2.1.1.4.0" -> {:ok, {:octet_string, "admin"}}
|
||||
"1.3.6.1.2.1.1.5.0" -> {:ok, {:octet_string, "router"}}
|
||||
"1.3.6.1.2.1.1.6.0" -> {:ok, {:octet_string, "office"}}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock MikroTik OIDs returning error
|
||||
expect(SnmpMock, :get, 5, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
assert {:ok, info} = Mikrotik.discover_system_info(@test_opts)
|
||||
|
||||
# Should still have base info
|
||||
assert info.sys_descr == "RouterOS Device"
|
||||
assert info.sys_name == "router"
|
||||
end
|
||||
end
|
||||
|
||||
describe "discover_interfaces/1" do
|
||||
test "delegates to Base.discover_interfaces" do
|
||||
# Mock IF-MIB walk for interface indices
|
||||
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: 1},
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.1.2", value: 2}
|
||||
]}
|
||||
end)
|
||||
|
||||
# Mock interface data for each interface
|
||||
expect(SnmpMock, :get, 16, fn _, oid, _ ->
|
||||
cond do
|
||||
String.ends_with?(oid, ".2.2.1.2.1") -> {:ok, {:octet_string, "ether1"}}
|
||||
String.ends_with?(oid, ".2.2.1.2.2") -> {:ok, {:octet_string, "ether2"}}
|
||||
String.ends_with?(oid, ".2.2.1.3.1") -> {:ok, {:integer, 6}}
|
||||
String.ends_with?(oid, ".2.2.1.3.2") -> {:ok, {:integer, 6}}
|
||||
String.ends_with?(oid, ".2.2.1.5.1") -> {:ok, {:gauge32, 1_000_000_000}}
|
||||
String.ends_with?(oid, ".2.2.1.5.2") -> {:ok, {:gauge32, 1_000_000_000}}
|
||||
String.ends_with?(oid, ".2.2.1.6.1") -> {:ok, {:octet_string, <<0x00, 0x0C, 0x42, 0xAB, 0xCD, 0xEF>>}}
|
||||
String.ends_with?(oid, ".2.2.1.6.2") -> {:ok, {:octet_string, <<0x00, 0x0C, 0x42, 0x12, 0x34, 0x56>>}}
|
||||
String.ends_with?(oid, ".2.2.1.7.1") -> {:ok, {:integer, 1}}
|
||||
String.ends_with?(oid, ".2.2.1.7.2") -> {:ok, {:integer, 1}}
|
||||
String.ends_with?(oid, ".2.2.1.8.1") -> {:ok, {:integer, 1}}
|
||||
String.ends_with?(oid, ".2.2.1.8.2") -> {:ok, {:integer, 1}}
|
||||
String.ends_with?(oid, ".31.1.1.1.1.1") -> {:ok, {:octet_string, "ether1"}}
|
||||
String.ends_with?(oid, ".31.1.1.1.1.2") -> {:ok, {:octet_string, "ether2"}}
|
||||
String.ends_with?(oid, ".31.1.1.1.18.1") -> {:ok, {:octet_string, "WAN"}}
|
||||
String.ends_with?(oid, ".31.1.1.1.18.2") -> {:ok, {:octet_string, "LAN"}}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, interfaces} = Mikrotik.discover_interfaces(@test_opts)
|
||||
|
||||
assert length(interfaces) == 2
|
||||
assert Enum.any?(interfaces, &(&1.if_descr == "ether1"))
|
||||
assert Enum.any?(interfaces, &(&1.if_descr == "ether2"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "discover_sensors/1" do
|
||||
test "discovers health sensors (temperature, voltage, fan)" do
|
||||
# Mock health sensor OIDs
|
||||
expect(SnmpMock, :get, 15, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Voltages
|
||||
"1.3.6.1.4.1.14988.1.1.3.1.0" -> {:ok, {:integer, 12}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.2.0" -> {:ok, {:integer, 33}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.3.0" -> {:ok, {:integer, 50}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.4.0" -> {:ok, {:integer, 120}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.8.0" -> {:ok, {:integer, 240}}
|
||||
# Temperatures
|
||||
"1.3.6.1.4.1.14988.1.1.3.5.0" -> {:ok, {:integer, 450}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.6.0" -> {:ok, {:integer, 550}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.7.0" -> {:ok, {:integer, 480}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.10.0" -> {:ok, {:integer, 500}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.11.0" -> {:ok, {:integer, 520}}
|
||||
# Power & Current
|
||||
"1.3.6.1.4.1.14988.1.1.3.13.0" -> {:ok, {:integer, 1500}}
|
||||
"1.3.6.1.4.1.14988.1.1.3.12.0" -> {:ok, {:integer, 250}}
|
||||
# Fans
|
||||
"1.3.6.1.4.1.14988.1.1.3.17.0" -> {:ok, {:integer, 3000}}
|
||||
"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}}
|
||||
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
|
||||
# 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}]}
|
||||
# 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}]}
|
||||
# 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}
|
||||
# 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}
|
||||
# Entity sensors from Base
|
||||
"1.3.6.1.2.1.99.1.1.1.1" -> {:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)
|
||||
|
||||
# Should have temperature sensors
|
||||
temp_sensors = Enum.filter(sensors, &String.contains?(&1.sensor_type, "temperature"))
|
||||
assert length(temp_sensors) > 0
|
||||
|
||||
# Should have voltage sensors
|
||||
voltage_sensors = Enum.filter(sensors, &String.contains?(&1.sensor_type, "voltage"))
|
||||
assert length(voltage_sensors) > 0
|
||||
|
||||
# Should have fan sensors
|
||||
fan_sensors = Enum.filter(sensors, &(&1.sensor_type == "fan1"))
|
||||
assert length(fan_sensors) > 0
|
||||
|
||||
# Check one sensor in detail
|
||||
cpu_temp = Enum.find(sensors, &(&1.sensor_type == "cpu_temperature"))
|
||||
assert cpu_temp
|
||||
assert cpu_temp.sensor_unit == "°C"
|
||||
assert cpu_temp.last_value == 55.0
|
||||
assert cpu_temp.status == "ok"
|
||||
end
|
||||
|
||||
test "discovers CPU load sensors" do
|
||||
# Mock health sensors (none available)
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
|
||||
# Mock all walk calls
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
# CPU load per core
|
||||
"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: 25},
|
||||
%{oid: "1.3.6.1.2.1.25.3.3.1.2.2", value: 50},
|
||||
%{oid: "1.3.6.1.2.1.25.3.3.1.2.3", value: 75},
|
||||
%{oid: "1.3.6.1.2.1.25.3.3.1.2.4", value: 95}
|
||||
]}
|
||||
|
||||
# Storage, POE, optical, entity as empty
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)
|
||||
|
||||
cpu_sensors = Enum.filter(sensors, &(&1.sensor_type == "cpu_load"))
|
||||
assert length(cpu_sensors) == 4
|
||||
|
||||
# Check status determination
|
||||
cpu1 = Enum.find(sensors, &(&1.sensor_index == "cpu1"))
|
||||
assert cpu1.status == "ok"
|
||||
assert cpu1.last_value == 25.0
|
||||
|
||||
cpu4 = Enum.find(sensors, &(&1.sensor_index == "cpu4"))
|
||||
assert cpu4.status == "critical"
|
||||
assert cpu4.last_value == 95.0
|
||||
end
|
||||
|
||||
test "discovers storage sensors with proper status" do
|
||||
# Mock health sensors (none)
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
|
||||
# Mock all walk calls
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
# CPU (empty)
|
||||
"1.3.6.1.2.1.25.3.3.1.2" ->
|
||||
{:ok, []}
|
||||
|
||||
# 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: "Physical memory"},
|
||||
%{oid: "1.3.6.1.2.1.25.2.3.1.3.2", value: "/disk1"}
|
||||
]}
|
||||
|
||||
"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: 8192},
|
||||
%{oid: "1.3.6.1.2.1.25.2.3.1.5.2", value: 10_000}
|
||||
]}
|
||||
|
||||
"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: 7168},
|
||||
%{oid: "1.3.6.1.2.1.25.2.3.1.6.2", value: 9600}
|
||||
]}
|
||||
|
||||
# POE, optical, entity as empty
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)
|
||||
|
||||
memory_sensor = Enum.find(sensors, &(&1.sensor_type == "memory_usage"))
|
||||
assert memory_sensor
|
||||
assert memory_sensor.status == "warning"
|
||||
assert memory_sensor.last_value == 87.5
|
||||
|
||||
disk_sensor = Enum.find(sensors, &(&1.sensor_type == "disk_usage"))
|
||||
assert disk_sensor
|
||||
assert disk_sensor.status == "critical"
|
||||
assert disk_sensor.last_value == 96.0
|
||||
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 all walk calls
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
# CPU sensors
|
||||
"1.3.6.1.2.1.25.3.3.1.2" ->
|
||||
{:ok, []}
|
||||
|
||||
# Storage sensors
|
||||
"1.3.6.1.2.1.25.2.3.1.3" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.2.1.25.2.3.1.5" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.2.1.25.2.3.1.6" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
# POE sensors
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.3" ->
|
||||
{:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.3.1", value: "ether1"}]}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.5" ->
|
||||
{:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.5.1", value: 480}]}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.6" ->
|
||||
{:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.6.1", value: 500}]}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.7" ->
|
||||
{:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.7.1", value: 240}]}
|
||||
|
||||
# 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}
|
||||
|
||||
# Entity sensors from Base
|
||||
"1.3.6.1.2.1.99.1.1.1.1" ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)
|
||||
|
||||
poe_sensors = Enum.filter(sensors, &String.contains?(&1.sensor_type, "poe_"))
|
||||
assert length(poe_sensors) == 3
|
||||
|
||||
voltage = Enum.find(sensors, &(&1.sensor_type == "poe_voltage"))
|
||||
assert voltage.last_value == 48.0
|
||||
assert voltage.sensor_unit == "V"
|
||||
end
|
||||
end
|
||||
|
||||
describe "identify_device/1" do
|
||||
test "identifies MikroTik device from sysDescr with model" do
|
||||
system_info = %{
|
||||
sys_descr: "RouterOS RB5009UG+S+ (stable) 7.13.2",
|
||||
sys_name: "router1",
|
||||
firmware_version: "7.13.2"
|
||||
}
|
||||
|
||||
result = Mikrotik.identify_device(system_info)
|
||||
|
||||
assert result.manufacturer == "MikroTik"
|
||||
assert result.model == "RB5009UG+S+"
|
||||
assert result.firmware_version == "7.13.2"
|
||||
end
|
||||
|
||||
test "identifies generic RouterOS device when model not found" do
|
||||
system_info = %{
|
||||
sys_descr: "RouterOS software version 7.12",
|
||||
sys_name: "router1"
|
||||
}
|
||||
|
||||
result = Mikrotik.identify_device(system_info)
|
||||
|
||||
assert result.manufacturer == "MikroTik"
|
||||
assert result.model == "RouterOS Device"
|
||||
end
|
||||
|
||||
test "extracts firmware from sysDescr when not in system_info" do
|
||||
system_info = %{
|
||||
sys_descr: "RouterOS RB2011 version 6.49.7"
|
||||
}
|
||||
|
||||
result = Mikrotik.identify_device(system_info)
|
||||
|
||||
assert result.manufacturer == "MikroTik"
|
||||
assert result.model == "RB2011"
|
||||
assert result.firmware_version == "6.49.7"
|
||||
end
|
||||
|
||||
property "always returns manufacturer and model" do
|
||||
check all(sys_descr <- StreamData.string(:alphanumeric)) do
|
||||
system_info = %{sys_descr: sys_descr}
|
||||
|
||||
result = Mikrotik.identify_device(system_info)
|
||||
|
||||
assert Map.has_key?(result, :manufacturer)
|
||||
assert Map.has_key?(result, :model)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "sensor status determination" do
|
||||
test "CPU status changes with load" do
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
"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: 60},
|
||||
%{oid: "1.3.6.1.2.1.25.3.3.1.2.2", value: 85},
|
||||
%{oid: "1.3.6.1.2.1.25.3.3.1.2.3", value: 95}
|
||||
]}
|
||||
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, sensors} = Mikrotik.discover_sensors(@test_opts)
|
||||
|
||||
cpu1 = Enum.find(sensors, &(&1.sensor_index == "cpu1"))
|
||||
cpu2 = Enum.find(sensors, &(&1.sensor_index == "cpu2"))
|
||||
cpu3 = Enum.find(sensors, &(&1.sensor_index == "cpu3"))
|
||||
|
||||
assert cpu1.status == "ok"
|
||||
assert cpu2.status == "warning"
|
||||
assert cpu3.status == "critical"
|
||||
end
|
||||
|
||||
test "memory status changes with usage percentage" do
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.25.3.3.1.2" -> {:ok, []}
|
||||
"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: "memory"}]}
|
||||
"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: 970}]}
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, sensors} = Mikrotik.discover_sensors(@test_opts)
|
||||
|
||||
memory = Enum.find(sensors, &(&1.sensor_type == "memory_usage"))
|
||||
assert memory.status == "critical"
|
||||
assert memory.last_value == 97.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +1,25 @@
|
|||
defmodule ToweropsWeb.EquipmentLiveTest do
|
||||
use ToweropsWeb.ConnCase
|
||||
|
||||
import Mox
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Towerops.Snmp.SnmpMock
|
||||
|
||||
setup :register_and_log_in_user
|
||||
setup :verify_on_exit!
|
||||
|
||||
# Set up SNMP mock globally to allow background tasks
|
||||
setup :set_mox_global
|
||||
|
||||
setup do
|
||||
# Stub SNMP mock globally to allow background task calls
|
||||
Mox.stub(SnmpMock, :get, fn _target, _oid, _opts -> {:error, :timeout} end)
|
||||
Mox.stub(SnmpMock, :walk, fn _target, _oid, _opts -> {:error, :timeout} end)
|
||||
Mox.stub(SnmpMock, :get_bulk, fn _target, _oid, _opts -> {:error, :timeout} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
setup %{user: user} do
|
||||
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue