diff --git a/lib/towerops_web/components/layouts.ex b/lib/towerops_web/components/layouts.ex index 52e5f57c..35d47f77 100644 --- a/lib/towerops_web/components/layouts.ex +++ b/lib/towerops_web/components/layouts.ex @@ -169,7 +169,7 @@ defmodule ToweropsWeb.Layouts do
- Before adding device, you need to create at least one site location. -
-- Sites help you organize your devices by physical location. -
-- Get started by adding your first device. -
-| - Drag handle - | -- Name - | -- IP Address - | -- Status - | -- Last Checked - | -
|---|---|---|---|---|
| - - | -
-
-
-
- <.link
- navigate={~p"/sites/#{site.id}"}
- class="text-sm font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
- >
- {site.name}
-
- ·
-
- {stats.total} {if stats.total == 1, do: "device", else: "devices"}
-
- 0}
- class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
- >
- {stats.up} up
-
- 0}
- class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
- >
- {stats.down} down
-
-
- - <.icon name="hero-map-pin" class="inline h-3 w-3" /> {site.location} - - |
- |||
| - - | -- {device.name} - | -- {device.ip_address} - | -- - {device.status |> to_string() |> String.upcase()} - - | -- <.timestamp datetime={device.last_checked_at} timezone={@timezone} /> - | -
+ Before adding device, you need to create at least one site location. +
++ Sites help you organize your devices by physical location. +
++ Get started by adding your first device. +
+| + Drag handle + | ++ Name + | ++ IP Address + | ++ Status + | ++ Last Checked + | +
|---|---|---|---|---|
| + + | +
+
+
+
+ <.link
+ navigate={~p"/sites/#{site.id}"}
+ class="text-sm font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
+ >
+ {site.name}
+
+ ·
+
+ {stats.total} {if stats.total == 1, do: "device", else: "devices"}
+
+ 0}
+ class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
+ >
+ {stats.up} up
+
+ 0}
+ class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
+ >
+ {stats.down} down
+
+
+ + <.icon name="hero-map-pin" class="inline h-3 w-3" /> {site.location} + + |
+ |||
| + + | ++ {device.name} + | ++ {device.ip_address} + | ++ + {device.status |> to_string() |> String.upcase()} + + | ++ <.timestamp datetime={device.last_checked_at} timezone={@timezone} /> + | +
+ Devices will appear here as they are discovered via LLDP/CDP. +
+No agents configured yet. <.link - navigate={~p"/orgs/#{@organization.slug}/agents"} + navigate={~p"/agents"} class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > Create an agent diff --git a/test/snmpkit/snmp_mgr_integration_test.exs b/test/snmpkit/snmp_mgr_integration_test.exs new file mode 100644 index 00000000..5ff8b9ca --- /dev/null +++ b/test/snmpkit/snmp_mgr_integration_test.exs @@ -0,0 +1,512 @@ +defmodule SnmpKit.SnmpMgrIntegrationTest do + use ExUnit.Case, async: false + + alias SnmpKit.SnmpMgr + + @moduletag :integration + @moduletag :snmpkit + + describe "SnmpMgr get/3" do + test "successfully retrieves sysUpTime from localhost simulator" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.3.0" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get(target, oid, opts) do + {:ok, value} -> + assert is_tuple(value) + {type, _data} = value + assert type == :timeticks + + {:error, :timeout} -> + # Simulator not running, skip test + :ok + + {:error, reason} -> + flunk("Unexpected error: #{inspect(reason)}") + end + end + + test "handles string OID format" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.1.0" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + result = SnmpMgr.get(target, oid, opts) + assert match?({:ok, _}, result) or match?({:error, :timeout}, result) + end + + test "handles list OID format" do + target = "127.0.0.1" + oid = [1, 3, 6, 1, 2, 1, 1, 1, 0] + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + result = SnmpMgr.get(target, oid, opts) + assert match?({:ok, _}, result) or match?({:error, :timeout}, result) + end + + test "returns error for invalid OID" do + target = "127.0.0.1" + oid = "1.3.6.1.99.99.99.99" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get(target, oid, opts) do + {:error, _reason} -> :ok + {:ok, _} -> flunk("Expected error for invalid OID") + end + end + + test "handles timeout gracefully" do + target = "192.0.2.1" + # RFC 5737 TEST-NET-1 address that should not route + oid = "1.3.6.1.2.1.1.1.0" + opts = [community: "public", version: :v2c, port: 161, timeout: 1000] + + assert {:error, :timeout} = SnmpMgr.get(target, oid, opts) + end + + test "returns error for wrong community string" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.1.0" + opts = [community: "wrong_community", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get(target, oid, opts) do + {:error, _reason} -> + :ok + + {:ok, _} -> + # Simulator may allow any community + :ok + end + end + end + + describe "SnmpMgr walk/3" do + test "successfully walks interface table" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.2.2.1.2" + opts = [community: "public", version: :v2c, port: 1161, timeout: 10_000] + + case SnmpMgr.walk(target, oid, opts) do + {:ok, results} -> + assert is_list(results) + + case results do + [first | _rest] -> + # Verify structure of results + assert is_map(first) + assert Map.has_key?(first, :oid) + assert Map.has_key?(first, :value) + + [] -> + :ok + end + + {:error, :timeout} -> + # Simulator not running + :ok + + {:error, reason} -> + # Other errors are acceptable (no such object, etc.) + assert is_atom(reason) + end + end + + test "returns empty list for non-existent subtree" do + target = "127.0.0.1" + oid = "1.3.6.1.99.99.99" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.walk(target, oid, opts) do + {:ok, results} -> + assert is_list(results) + assert results == [] + + {:error, _reason} -> + # Error is also acceptable + :ok + end + end + + test "handles walk timeout" do + target = "192.0.2.1" + oid = "1.3.6.1.2.1.2.2.1" + opts = [community: "public", version: :v2c, port: 161, timeout: 1000] + + assert {:error, :timeout} = SnmpMgr.walk(target, oid, opts) + end + + test "walk result format is consistent" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.walk(target, oid, opts) do + {:ok, results} when is_list(results) -> + # All results should be maps with :oid and :value keys + for result <- results do + assert is_map(result) + assert Map.has_key?(result, :oid) + assert Map.has_key?(result, :value) + assert is_binary(result.oid) or is_list(result.oid) + end + + {:error, :timeout} -> + :ok + + {:error, _} -> + :ok + end + end + end + + describe "SnmpMgr get_bulk/3" do + test "successfully performs get_bulk operation" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.2.2.1.10" + + opts = [ + community: "public", + version: :v2c, + port: 1161, + timeout: 5000, + max_repetitions: 10 + ] + + case SnmpMgr.get_bulk(target, oid, opts) do + {:ok, results} -> + assert is_list(results) + + case results do + [first | _rest] -> + assert is_map(first) + assert Map.has_key?(first, :oid) + assert Map.has_key?(first, :value) + + [] -> + :ok + end + + {:error, :timeout} -> + :ok + + {:error, reason} -> + assert is_atom(reason) + end + end + + test "respects max_repetitions parameter" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.2.2.1" + + opts = [ + community: "public", + version: :v2c, + port: 1161, + timeout: 5000, + max_repetitions: 5 + ] + + case SnmpMgr.get_bulk(target, oid, opts) do + {:ok, results} -> + # Should get at most max_repetitions results per column + assert is_list(results) + + {:error, :timeout} -> + :ok + + {:error, _} -> + :ok + end + end + + test "get_bulk only works with SNMPv2c" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.2.2.1.10" + + # Try with v1 - should not support get_bulk + opts_v1 = [community: "public", version: :v1, port: 1161, timeout: 5000] + + case SnmpMgr.get_bulk(target, oid, opts_v1) do + {:error, _reason} -> + # Expected - v1 doesn't support get_bulk + :ok + + {:ok, _} -> + # Some implementations may allow it or fall back + :ok + end + + # Try with v2c - should work + opts_v2c = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get_bulk(target, oid, opts_v2c) do + {:ok, results} -> + assert is_list(results) + + {:error, :timeout} -> + :ok + + {:error, _} -> + :ok + end + end + end + + describe "SnmpMgr with different versions" do + test "SNMPv1 get operation" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.3.0" + opts = [community: "public", version: :v1, port: 1161, timeout: 5000] + + result = SnmpMgr.get(target, oid, opts) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + + test "SNMPv2c get operation" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.3.0" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + result = SnmpMgr.get(target, oid, opts) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + end + + describe "SnmpMgr error handling" do + test "handles network errors gracefully" do + target = "192.0.2.1" + oid = "1.3.6.1.2.1.1.1.0" + opts = [community: "public", version: :v2c, port: 161, timeout: 1000] + + assert {:error, :timeout} = SnmpMgr.get(target, oid, opts) + end + + test "handles invalid target format" do + target = "not-a-valid-ip" + oid = "1.3.6.1.2.1.1.1.0" + opts = [community: "public", version: :v2c, port: 161, timeout: 1000] + + case SnmpMgr.get(target, oid, opts) do + {:error, _reason} -> + :ok + + {:ok, _} -> + :ok + # Some systems may resolve hostnames + end + end + + test "handles malformed OID" do + target = "127.0.0.1" + # Invalid OID format + oid = "not.an.oid" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get(target, oid, opts) do + {:error, _reason} -> + # Expected + :ok + + {:ok, _} -> + # Some parsers may handle this + :ok + end + end + end + + describe "SnmpMgr real-world scenarios" do + test "retrieve system information bundle" do + target = "127.0.0.1" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + system_oids = [ + "1.3.6.1.2.1.1.1.0", + # sysDescr + "1.3.6.1.2.1.1.2.0", + # sysObjectID + "1.3.6.1.2.1.1.3.0", + # sysUpTime + "1.3.6.1.2.1.1.4.0", + # sysContact + "1.3.6.1.2.1.1.5.0", + # sysName + "1.3.6.1.2.1.1.6.0" + # sysLocation + ] + + # Try to get all system info + results = + Enum.map(system_oids, fn oid -> + SnmpMgr.get(target, oid, opts) + end) + + # At least some should succeed (if simulator running) or all timeout + assert Enum.all?(results, fn result -> + match?({:ok, _}, result) or match?({:error, _}, result) + end) + end + + test "walk and count interfaces" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.2.2.1.1" + # ifIndex + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.walk(target, oid, opts) do + {:ok, results} -> + interface_count = length(results) + assert interface_count >= 0 + + {:error, :timeout} -> + :ok + + {:error, _} -> + :ok + end + end + + test "retrieve interface statistics" do + target = "127.0.0.1" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + # Common interface stats OIDs + stats_oids = [ + "1.3.6.1.2.1.2.2.1.10.1", + # ifInOctets for interface 1 + "1.3.6.1.2.1.2.2.1.16.1", + # ifOutOctets for interface 1 + "1.3.6.1.2.1.2.2.1.8.1" + # ifOperStatus for interface 1 + ] + + results = + Enum.map(stats_oids, fn oid -> + SnmpMgr.get(target, oid, opts) + end) + + assert Enum.all?(results, fn result -> + match?({:ok, _}, result) or match?({:error, _}, result) + end) + end + end + + describe "SnmpMgr performance and reliability" do + test "handles multiple concurrent requests" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.3.0" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + tasks = + for _ <- 1..5 do + Task.async(fn -> SnmpMgr.get(target, oid, opts) end) + end + + results = Task.await_many(tasks, 10_000) + + # All requests should complete + assert length(results) == 5 + + # All should either succeed or have consistent errors + assert Enum.all?(results, fn result -> + match?({:ok, _}, result) or match?({:error, _}, result) + end) + end + + test "respects timeout parameter" do + target = "192.0.2.1" + oid = "1.3.6.1.2.1.1.1.0" + opts = [community: "public", version: :v2c, port: 161, timeout: 500] + + start_time = System.monotonic_time(:millisecond) + result = SnmpMgr.get(target, oid, opts) + end_time = System.monotonic_time(:millisecond) + + assert {:error, :timeout} = result + + # Should timeout around the specified timeout value + duration = end_time - start_time + # Allow some overhead + assert duration < 2000 + end + + test "handles rapid sequential requests" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.3.0" + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + results = + for _ <- 1..10 do + SnmpMgr.get(target, oid, opts) + end + + # All should complete + assert length(results) == 10 + + # All should have consistent results + assert Enum.all?(results, fn result -> + match?({:ok, _}, result) or match?({:error, _}, result) + end) + end + end + + describe "SnmpMgr data type handling" do + test "correctly handles timeticks values" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.3.0" + # sysUpTime + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get(target, oid, opts) do + {:ok, value} -> + assert is_tuple(value) + assert elem(value, 0) == :timeticks + + {:error, :timeout} -> + :ok + + {:error, _} -> + :ok + end + end + + test "correctly handles octet string values" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.1.1.0" + # sysDescr + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get(target, oid, opts) do + {:ok, value} -> + assert is_tuple(value) + # Could be :octet_string + type = elem(value, 0) + assert is_atom(type) + + {:error, :timeout} -> + :ok + + {:error, _} -> + :ok + end + end + + test "correctly handles integer values" do + target = "127.0.0.1" + oid = "1.3.6.1.2.1.2.2.1.8.1" + # ifOperStatus + opts = [community: "public", version: :v2c, port: 1161, timeout: 5000] + + case SnmpMgr.get(target, oid, opts) do + {:ok, value} -> + assert is_tuple(value) or is_integer(value) + + {:error, :timeout} -> + :ok + + {:error, _} -> + :ok + end + end + end +end