3081 lines
89 KiB
Elixir
3081 lines
89 KiB
Elixir
defmodule Towerops.SnmpTest do
|
|
use Towerops.DataCase
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Snmp
|
|
alias Towerops.Snmp.ArpEntry
|
|
alias Towerops.Snmp.Device
|
|
alias Towerops.Snmp.Interface
|
|
alias Towerops.Snmp.InterfaceStat
|
|
alias Towerops.Snmp.MacAddress
|
|
alias Towerops.Snmp.Neighbor
|
|
alias Towerops.Snmp.Processor
|
|
alias Towerops.Snmp.Sensor
|
|
alias Towerops.Snmp.SensorReading
|
|
alias Towerops.Snmp.SnmpMock
|
|
alias Towerops.Snmp.Storage
|
|
alias Towerops.Snmp.StorageReading
|
|
alias Towerops.Snmp.WirelessClient
|
|
alias Towerops.Snmp.WirelessClientReading
|
|
|
|
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
|
|
})
|
|
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Test Router",
|
|
ip_address: "192.168.1.1",
|
|
snmp_enabled: true,
|
|
snmp_version: "2c",
|
|
snmp_community: "public",
|
|
snmp_port: 161,
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
snmp_device =
|
|
%Device{}
|
|
|> Device.changeset(%{
|
|
device_id: device.id,
|
|
sys_name: "test-router",
|
|
sys_descr: "Test Device"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%{device: device, snmp_device: snmp_device, organization: organization}
|
|
end
|
|
|
|
describe "test_connection/4" do
|
|
import Mox
|
|
|
|
setup :verify_on_exit!
|
|
|
|
test "delegates to Client.test_connection" do
|
|
expect(SnmpMock, :get, fn _, _, _ ->
|
|
{:ok, 12_345}
|
|
end)
|
|
|
|
result = Snmp.test_connection("192.168.1.1", "public", "2c")
|
|
|
|
case result do
|
|
{:ok, _} -> assert true
|
|
{:error, _} -> assert true
|
|
end
|
|
end
|
|
|
|
test "accepts custom port" do
|
|
expect(SnmpMock, :get, fn _, _, _ ->
|
|
{:ok, 12_345}
|
|
end)
|
|
|
|
result = Snmp.test_connection("192.168.1.1", "public", "2c", 1161)
|
|
|
|
case result do
|
|
{:ok, _} -> assert true
|
|
{:error, _} -> assert true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "get_device/1" do
|
|
test "returns device for device_id", %{device: device, snmp_device: snmp_device} do
|
|
found_device = Snmp.get_device(device.id)
|
|
assert found_device.id == snmp_device.id
|
|
assert found_device.sys_name == "test-router"
|
|
end
|
|
|
|
test "returns nil for non-existent device_id" do
|
|
assert Snmp.get_device(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "get_device_with_associations/1" do
|
|
test "returns device with preloaded associations", %{device: device, snmp_device: snmp_device} do
|
|
found_device = Snmp.get_device_with_associations(device.id)
|
|
assert found_device.id == snmp_device.id
|
|
assert Ecto.assoc_loaded?(found_device.interfaces)
|
|
assert Ecto.assoc_loaded?(found_device.sensors)
|
|
end
|
|
|
|
test "returns nil for non-existent device_id" do
|
|
assert Snmp.get_device_with_associations(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "list_interfaces/1" do
|
|
test "returns all interfaces for a device ordered by if_index", %{device: _device, snmp_device: snmp_device} do
|
|
interface1 =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 2,
|
|
if_name: "eth1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interface2 =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interfaces = Snmp.list_interfaces(snmp_device.id)
|
|
assert length(interfaces) == 2
|
|
assert hd(interfaces).id == interface2.id
|
|
assert List.last(interfaces).id == interface1.id
|
|
end
|
|
|
|
test "returns empty list for device with no interfaces", %{device: _device, snmp_device: snmp_device} do
|
|
assert Snmp.list_interfaces(snmp_device.id) == []
|
|
end
|
|
end
|
|
|
|
describe "list_monitored_interfaces/1" do
|
|
test "returns only monitored interfaces", %{device: _device, snmp_device: snmp_device} do
|
|
monitored =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
monitored: true
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_unmonitored =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 2,
|
|
if_name: "eth1",
|
|
monitored: false
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interfaces = Snmp.list_monitored_interfaces(snmp_device.id)
|
|
assert length(interfaces) == 1
|
|
assert hd(interfaces).id == monitored.id
|
|
end
|
|
end
|
|
|
|
describe "get_interface/1" do
|
|
test "returns interface by id", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
found = Snmp.get_interface(interface.id)
|
|
assert found.id == interface.id
|
|
assert found.if_name == "eth0"
|
|
end
|
|
|
|
test "returns nil for non-existent id" do
|
|
assert Snmp.get_interface(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "update_interface/2" do
|
|
test "updates interface attributes", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
monitored: false
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, updated} = Snmp.update_interface(interface, %{monitored: true})
|
|
assert updated.monitored == true
|
|
end
|
|
|
|
test "returns error for invalid attributes", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:error, changeset} = Snmp.update_interface(interface, %{if_index: nil})
|
|
assert changeset.errors[:if_index]
|
|
end
|
|
end
|
|
|
|
describe "list_sensors/1" do
|
|
test "returns all sensors for a device ordered by type and index", %{device: _device, snmp_device: snmp_device} do
|
|
sensor1 =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
sensor2 =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "power",
|
|
sensor_index: "2",
|
|
sensor_oid: "1.2.3.5"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
sensors = Snmp.list_sensors(snmp_device.id)
|
|
assert length(sensors) == 2
|
|
assert sensor1.id in Enum.map(sensors, & &1.id)
|
|
assert sensor2.id in Enum.map(sensors, & &1.id)
|
|
end
|
|
|
|
test "returns empty list for device with no sensors", %{device: _device, snmp_device: snmp_device} do
|
|
assert Snmp.list_sensors(snmp_device.id) == []
|
|
end
|
|
end
|
|
|
|
describe "list_monitored_sensors/1" do
|
|
test "returns only monitored sensors", %{device: _device, snmp_device: snmp_device} do
|
|
monitored =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4",
|
|
monitored: true
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_unmonitored =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "power",
|
|
sensor_index: "2",
|
|
sensor_oid: "1.2.3.5",
|
|
monitored: false
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
sensors = Snmp.list_monitored_sensors(snmp_device.id)
|
|
assert length(sensors) == 1
|
|
assert hd(sensors).id == monitored.id
|
|
end
|
|
end
|
|
|
|
describe "list_sensors_by_type/1" do
|
|
test "groups sensors by type", %{device: _device, snmp_device: snmp_device} do
|
|
_temp1 =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_temp2 =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "2",
|
|
sensor_oid: "1.2.3.5"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_power =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "power",
|
|
sensor_index: "3",
|
|
sensor_oid: "1.2.3.6"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
grouped = Snmp.list_sensors_by_type(snmp_device.id)
|
|
assert Map.has_key?(grouped, "temperature")
|
|
assert Map.has_key?(grouped, "power")
|
|
assert length(grouped["temperature"]) == 2
|
|
assert length(grouped["power"]) == 1
|
|
end
|
|
|
|
test "returns empty map for device with no sensors", %{device: _device, snmp_device: snmp_device} do
|
|
assert Snmp.list_sensors_by_type(snmp_device.id) == %{}
|
|
end
|
|
end
|
|
|
|
describe "get_sensor/1" do
|
|
test "returns sensor by id", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
found = Snmp.get_sensor(sensor.id)
|
|
assert found.id == sensor.id
|
|
assert found.sensor_type == "temperature"
|
|
end
|
|
|
|
test "returns nil for non-existent id" do
|
|
assert Snmp.get_sensor(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "update_sensor/2" do
|
|
test "updates sensor attributes", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4",
|
|
monitored: false
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, updated} = Snmp.update_sensor(sensor, %{monitored: true})
|
|
assert updated.monitored == true
|
|
end
|
|
|
|
test "returns error for invalid attributes", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:error, changeset} = Snmp.update_sensor(sensor, %{sensor_oid: nil})
|
|
assert changeset.errors[:sensor_oid]
|
|
end
|
|
end
|
|
|
|
describe "get_sensor_readings/2" do
|
|
test "returns recent readings for a sensor", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
for i <- 1..5 do
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 20.0 + i,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
readings = Snmp.get_sensor_readings(sensor.id)
|
|
assert length(readings) == 5
|
|
end
|
|
|
|
test "respects limit option", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
for i <- 1..10 do
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 20.0 + i,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
readings = Snmp.get_sensor_readings(sensor.id, limit: 3)
|
|
assert length(readings) == 3
|
|
end
|
|
|
|
test "respects since option", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
past = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
recent = DateTime.add(DateTime.utc_now(), -60, :second)
|
|
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 20.0,
|
|
status: "ok",
|
|
checked_at: past
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 25.0,
|
|
status: "ok",
|
|
checked_at: recent
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
since_time = DateTime.add(DateTime.utc_now(), -120, :second)
|
|
readings = Snmp.get_sensor_readings(sensor.id, since: since_time)
|
|
assert length(readings) == 1
|
|
assert hd(readings).value == 25.0
|
|
end
|
|
|
|
test "returns empty list for sensor with no readings", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert Snmp.get_sensor_readings(sensor.id) == []
|
|
end
|
|
end
|
|
|
|
describe "get_latest_sensor_reading/1" do
|
|
test "returns most recent reading", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
old_reading =
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 20.0,
|
|
status: "ok",
|
|
checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_reading =
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 25.0,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
latest = Snmp.get_latest_sensor_reading(sensor.id)
|
|
assert latest.id == new_reading.id
|
|
refute latest.id == old_reading.id
|
|
end
|
|
|
|
test "returns nil for sensor with no readings", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert Snmp.get_latest_sensor_reading(sensor.id) == nil
|
|
end
|
|
end
|
|
|
|
describe "get_interface_stats/2" do
|
|
test "returns recent stats for an interface", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
for i <- 1..5 do
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: i * 1000,
|
|
if_out_octets: i * 2000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
stats = Snmp.get_interface_stats(interface.id)
|
|
assert length(stats) == 5
|
|
end
|
|
|
|
test "respects limit option", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
for i <- 1..10 do
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: i * 1000,
|
|
if_out_octets: i * 2000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
stats = Snmp.get_interface_stats(interface.id, limit: 3)
|
|
assert length(stats) == 3
|
|
end
|
|
|
|
test "respects since option", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
past = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
recent = DateTime.add(DateTime.utc_now(), -60, :second)
|
|
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: past
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 5000,
|
|
if_out_octets: 6000,
|
|
checked_at: recent
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
since_time = DateTime.add(DateTime.utc_now(), -120, :second)
|
|
stats = Snmp.get_interface_stats(interface.id, since: since_time)
|
|
assert length(stats) == 1
|
|
assert hd(stats).if_in_octets == 5000
|
|
end
|
|
|
|
test "returns empty list for interface with no stats", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert Snmp.get_interface_stats(interface.id) == []
|
|
end
|
|
end
|
|
|
|
describe "get_latest_interface_stat/1" do
|
|
test "returns most recent stat", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
old_stat =
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_stat =
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 5000,
|
|
if_out_octets: 6000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
latest = Snmp.get_latest_interface_stat(interface.id)
|
|
assert latest.id == new_stat.id
|
|
refute latest.id == old_stat.id
|
|
end
|
|
|
|
test "returns nil for interface with no stats", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert Snmp.get_latest_interface_stat(interface.id) == nil
|
|
end
|
|
end
|
|
|
|
describe "create_sensor_reading/1" do
|
|
test "creates a sensor reading with valid attributes", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, reading} =
|
|
Snmp.create_sensor_reading(%{
|
|
sensor_id: sensor.id,
|
|
value: 25.5,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
assert reading.sensor_id == sensor.id
|
|
assert reading.value == 25.5
|
|
assert reading.status == "ok"
|
|
end
|
|
|
|
test "returns error with invalid attributes" do
|
|
{:error, changeset} = Snmp.create_sensor_reading(%{value: "not a number"})
|
|
assert changeset.errors[:sensor_id]
|
|
end
|
|
end
|
|
|
|
describe "create_interface_stat/1" do
|
|
test "creates an interface stat with valid attributes", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, stat} =
|
|
Snmp.create_interface_stat(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
if_in_errors: 0,
|
|
if_out_errors: 0,
|
|
if_in_discards: 0,
|
|
if_out_discards: 0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
assert stat.interface_id == interface.id
|
|
assert stat.if_in_octets == 1000
|
|
assert stat.if_out_octets == 2000
|
|
end
|
|
|
|
test "returns error with invalid attributes" do
|
|
{:error, changeset} = Snmp.create_interface_stat(%{if_in_octets: "not a number"})
|
|
assert changeset.errors[:interface_id]
|
|
end
|
|
end
|
|
|
|
describe "get_latest_sensor_readings_batch/1" do
|
|
test "returns empty map for empty sensor list", %{device: _device, snmp_device: _snmp_device} do
|
|
assert Snmp.get_latest_sensor_readings_batch([]) == %{}
|
|
end
|
|
|
|
test "returns readings for multiple sensors", %{device: _device, snmp_device: snmp_device} do
|
|
sensor1 =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
sensor2 =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "power",
|
|
sensor_index: "2",
|
|
sensor_oid: "1.2.3.5"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
reading1 =
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor1.id,
|
|
value: 25.0,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
reading2 =
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor2.id,
|
|
value: 100.0,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_sensor_readings_batch([sensor1.id, sensor2.id])
|
|
assert map_size(batch) == 2
|
|
assert batch[sensor1.id].id == reading1.id
|
|
assert batch[sensor2.id].id == reading2.id
|
|
end
|
|
|
|
test "returns only latest reading per sensor", %{device: _device, snmp_device: snmp_device} do
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_old_reading =
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 20.0,
|
|
status: "ok",
|
|
checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_reading =
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor.id,
|
|
value: 25.0,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_sensor_readings_batch([sensor.id])
|
|
assert map_size(batch) == 1
|
|
assert batch[sensor.id].id == new_reading.id
|
|
assert batch[sensor.id].value == 25.0
|
|
end
|
|
|
|
test "excludes sensors without readings", %{device: _device, snmp_device: snmp_device} do
|
|
sensor_with_reading =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: "1.2.3.4"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
sensor_without_reading =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "power",
|
|
sensor_index: "2",
|
|
sensor_oid: "1.2.3.5"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(%{
|
|
sensor_id: sensor_with_reading.id,
|
|
value: 25.0,
|
|
status: "ok",
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_sensor_readings_batch([sensor_with_reading.id, sensor_without_reading.id])
|
|
assert map_size(batch) == 1
|
|
assert Map.has_key?(batch, sensor_with_reading.id)
|
|
refute Map.has_key?(batch, sensor_without_reading.id)
|
|
end
|
|
end
|
|
|
|
describe "get_latest_interface_stats_batch/1" do
|
|
test "returns empty map for empty interface list", %{device: _device, snmp_device: _snmp_device} do
|
|
assert Snmp.get_latest_interface_stats_batch([]) == %{}
|
|
end
|
|
|
|
test "returns stats for multiple interfaces", %{device: _device, snmp_device: snmp_device} do
|
|
interface1 =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interface2 =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 2,
|
|
if_name: "eth1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
stat1 =
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface1.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
stat2 =
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface2.id,
|
|
if_in_octets: 5000,
|
|
if_out_octets: 6000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_interface_stats_batch([interface1.id, interface2.id])
|
|
assert map_size(batch) == 2
|
|
assert batch[interface1.id].id == stat1.id
|
|
assert batch[interface2.id].id == stat2.id
|
|
end
|
|
|
|
test "returns only latest stat per interface", %{device: _device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_old_stat =
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_stat =
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface.id,
|
|
if_in_octets: 5000,
|
|
if_out_octets: 6000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_interface_stats_batch([interface.id])
|
|
assert map_size(batch) == 1
|
|
assert batch[interface.id].id == new_stat.id
|
|
assert batch[interface.id].if_in_octets == 5000
|
|
end
|
|
|
|
test "excludes interfaces without stats", %{device: _device, snmp_device: snmp_device} do
|
|
interface_with_stat =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interface_without_stat =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 2,
|
|
if_name: "eth1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(%{
|
|
interface_id: interface_with_stat.id,
|
|
if_in_octets: 1000,
|
|
if_out_octets: 2000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_interface_stats_batch([interface_with_stat.id, interface_without_stat.id])
|
|
assert map_size(batch) == 1
|
|
assert Map.has_key?(batch, interface_with_stat.id)
|
|
refute Map.has_key?(batch, interface_without_stat.id)
|
|
end
|
|
end
|
|
|
|
describe "list_neighbors/1" do
|
|
test "returns all neighbors for a device", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbor =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "neighbor-switch",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbors = Snmp.list_neighbors(device.id)
|
|
assert length(neighbors) == 1
|
|
assert hd(neighbors).id == neighbor.id
|
|
assert Ecto.assoc_loaded?(hd(neighbors).interface)
|
|
end
|
|
|
|
test "returns empty list for device with no neighbors", %{device: device, snmp_device: _snmp_device} do
|
|
assert Snmp.list_neighbors(device.id) == []
|
|
end
|
|
|
|
test "orders neighbors by protocol and system name", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbor_lldp_b =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "switch-b",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbor_cdp_a =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "cdp",
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
remote_system_name: "router-a",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbor_lldp_a =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "11:22:33:44:55:66",
|
|
remote_system_name: "switch-a",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbors = Snmp.list_neighbors(device.id)
|
|
assert length(neighbors) == 3
|
|
# Should be ordered: cdp first, then lldp alphabetically
|
|
assert Enum.at(neighbors, 0).id == neighbor_cdp_a.id
|
|
assert Enum.at(neighbors, 1).id == neighbor_lldp_a.id
|
|
assert Enum.at(neighbors, 2).id == neighbor_lldp_b.id
|
|
end
|
|
end
|
|
|
|
describe "get_neighbor/1" do
|
|
test "returns neighbor by id", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbor =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
found = Snmp.get_neighbor(neighbor.id)
|
|
assert found.id == neighbor.id
|
|
assert found.remote_chassis_id == "00:11:22:33:44:55"
|
|
end
|
|
|
|
test "returns nil for non-existent id" do
|
|
assert Snmp.get_neighbor(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "upsert_neighbor/1" do
|
|
test "creates new neighbor when none exists", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
attrs = %{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "new-neighbor",
|
|
last_discovered_at: DateTime.utc_now()
|
|
}
|
|
|
|
{:ok, neighbor} = Snmp.upsert_neighbor(attrs)
|
|
assert neighbor.remote_system_name == "new-neighbor"
|
|
assert neighbor.remote_chassis_id == "00:11:22:33:44:55"
|
|
end
|
|
|
|
test "updates existing neighbor when match found", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
existing =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "old-name",
|
|
last_discovered_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_time = Towerops.Time.now()
|
|
|
|
attrs = %{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
remote_system_name: "updated-name",
|
|
last_discovered_at: new_time
|
|
}
|
|
|
|
{:ok, updated} = Snmp.upsert_neighbor(attrs)
|
|
assert updated.id == existing.id
|
|
assert updated.remote_system_name == "updated-name"
|
|
# Compare with truncation since database stores :utc_datetime (no microseconds)
|
|
assert DateTime.compare(updated.last_discovered_at, new_time) in [:eq, :gt]
|
|
end
|
|
end
|
|
|
|
describe "delete_stale_neighbors/2" do
|
|
test "deletes neighbors older than cutoff", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
old_time = DateTime.add(DateTime.utc_now(), -7200, :second)
|
|
recent_time = DateTime.add(DateTime.utc_now(), -60, :second)
|
|
|
|
old_neighbor =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
last_discovered_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
recent_neighbor =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
protocol: "cdp",
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
last_discovered_at: recent_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
{deleted_count, _} = Snmp.delete_stale_neighbors(device.id, cutoff)
|
|
|
|
assert deleted_count == 1
|
|
assert Snmp.get_neighbor(old_neighbor.id) == nil
|
|
assert Snmp.get_neighbor(recent_neighbor.id)
|
|
end
|
|
|
|
test "does not delete neighbors from other devices", %{organization: organization} do
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Another Site",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device1} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device2} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 2",
|
|
ip_address: "192.168.1.2",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
snmp_device1 =
|
|
%Device{}
|
|
|> Device.changeset(%{
|
|
device_id: device1.id,
|
|
sys_name: "router1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
snmp_device2 =
|
|
%Device{}
|
|
|> Device.changeset(%{
|
|
device_id: device2.id,
|
|
sys_name: "router2"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interface1 =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device1.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
interface2 =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device2.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
old_time = DateTime.add(DateTime.utc_now(), -7200, :second)
|
|
|
|
neighbor1 =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device1.id,
|
|
interface_id: interface1.id,
|
|
protocol: "lldp",
|
|
last_discovered_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
neighbor2 =
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device2.id,
|
|
interface_id: interface2.id,
|
|
protocol: "lldp",
|
|
last_discovered_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
{deleted_count, _} = Snmp.delete_stale_neighbors(device1.id, cutoff)
|
|
|
|
assert deleted_count == 1
|
|
assert Snmp.get_neighbor(neighbor1.id) == nil
|
|
assert Snmp.get_neighbor(neighbor2.id)
|
|
end
|
|
end
|
|
|
|
describe "get_storage_readings/2" do
|
|
test "returns recent readings for a storage entry", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
for i <- 1..5 do
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 1000 * i,
|
|
total_bytes: 10_000,
|
|
usage_percent: 10.0 * i,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
readings = Snmp.get_storage_readings(storage.id)
|
|
assert length(readings) == 5
|
|
end
|
|
|
|
test "respects limit option", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
for i <- 1..10 do
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 1000 * i,
|
|
total_bytes: 10_000,
|
|
usage_percent: 10.0 * i,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
readings = Snmp.get_storage_readings(storage.id, limit: 3)
|
|
assert length(readings) == 3
|
|
end
|
|
|
|
test "respects since option", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
past = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
recent = DateTime.add(DateTime.utc_now(), -60, :second)
|
|
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 1000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 10.0,
|
|
checked_at: past
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 5000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 50.0,
|
|
checked_at: recent
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
since_time = DateTime.add(DateTime.utc_now(), -120, :second)
|
|
readings = Snmp.get_storage_readings(storage.id, since: since_time)
|
|
assert length(readings) == 1
|
|
assert hd(readings).usage_percent == 50.0
|
|
end
|
|
|
|
test "returns empty list for storage with no readings", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert Snmp.get_storage_readings(storage.id) == []
|
|
end
|
|
end
|
|
|
|
describe "get_latest_storage_reading/1" do
|
|
test "returns most recent reading", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
old_reading =
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 1000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 10.0,
|
|
checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_reading =
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 5000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 50.0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
latest = Snmp.get_latest_storage_reading(storage.id)
|
|
assert latest.id == new_reading.id
|
|
refute latest.id == old_reading.id
|
|
end
|
|
|
|
test "returns nil for storage with no readings", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert Snmp.get_latest_storage_reading(storage.id) == nil
|
|
end
|
|
end
|
|
|
|
describe "get_latest_storage_readings_batch/1" do
|
|
test "returns empty map for empty storage list" do
|
|
assert Snmp.get_latest_storage_readings_batch([]) == %{}
|
|
end
|
|
|
|
test "returns readings for multiple storage entries", %{snmp_device: snmp_device} do
|
|
storage1 =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
storage2 =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 2,
|
|
storage_type: "ram",
|
|
description: "Physical Memory"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
reading1 =
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage1.id,
|
|
used_bytes: 5000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 50.0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
reading2 =
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage2.id,
|
|
used_bytes: 2000,
|
|
total_bytes: 8000,
|
|
usage_percent: 25.0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_storage_readings_batch([storage1.id, storage2.id])
|
|
assert map_size(batch) == 2
|
|
assert batch[storage1.id].id == reading1.id
|
|
assert batch[storage2.id].id == reading2.id
|
|
end
|
|
|
|
test "returns only latest reading per storage", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_old_reading =
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 1000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 10.0,
|
|
checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_reading =
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 5000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 50.0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_storage_readings_batch([storage.id])
|
|
assert map_size(batch) == 1
|
|
assert batch[storage.id].id == new_reading.id
|
|
assert batch[storage.id].usage_percent == 50.0
|
|
end
|
|
|
|
test "excludes storage without readings", %{snmp_device: snmp_device} do
|
|
storage_with_reading =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
storage_without_reading =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 2,
|
|
storage_type: "ram",
|
|
description: "Physical Memory"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(%{
|
|
storage_id: storage_with_reading.id,
|
|
used_bytes: 5000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 50.0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
batch = Snmp.get_latest_storage_readings_batch([storage_with_reading.id, storage_without_reading.id])
|
|
assert map_size(batch) == 1
|
|
assert Map.has_key?(batch, storage_with_reading.id)
|
|
refute Map.has_key?(batch, storage_without_reading.id)
|
|
end
|
|
end
|
|
|
|
describe "create_storage_reading/1" do
|
|
test "creates a storage reading with valid attributes", %{snmp_device: snmp_device} do
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
storage_type: "fixed_disk",
|
|
description: "/dev/sda1"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, reading} =
|
|
Snmp.create_storage_reading(%{
|
|
storage_id: storage.id,
|
|
used_bytes: 5000,
|
|
total_bytes: 10_000,
|
|
usage_percent: 50.0,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
assert reading.storage_id == storage.id
|
|
assert reading.used_bytes == 5000
|
|
assert reading.total_bytes == 10_000
|
|
assert reading.usage_percent == 50.0
|
|
end
|
|
|
|
test "returns error with invalid attributes" do
|
|
{:error, changeset} = Snmp.create_storage_reading(%{used_bytes: 1000})
|
|
assert changeset.errors[:storage_id]
|
|
end
|
|
end
|
|
|
|
describe "list_arp_entries/1" do
|
|
test "returns all ARP entries for a device", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
arp =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
entry_type: "dynamic",
|
|
if_index: 1,
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
entries = Snmp.list_arp_entries(device.id)
|
|
assert length(entries) == 1
|
|
assert hd(entries).id == arp.id
|
|
assert Ecto.assoc_loaded?(hd(entries).interface)
|
|
end
|
|
|
|
test "returns empty list for device with no ARP entries", %{device: device} do
|
|
assert Snmp.list_arp_entries(device.id) == []
|
|
end
|
|
|
|
test "orders ARP entries by IP address", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
arp2 =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.200",
|
|
mac_address: "aa:bb:cc:dd:ee:02",
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
arp1 =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:01",
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
entries = Snmp.list_arp_entries(device.id)
|
|
assert length(entries) == 2
|
|
assert Enum.at(entries, 0).id == arp1.id
|
|
assert Enum.at(entries, 1).id == arp2.id
|
|
end
|
|
end
|
|
|
|
describe "get_arp_entry/1" do
|
|
test "returns ARP entry by id", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
arp =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
found = Snmp.get_arp_entry(arp.id)
|
|
assert found.id == arp.id
|
|
assert found.ip_address == "192.168.1.100"
|
|
end
|
|
|
|
test "returns nil for non-existent id" do
|
|
assert Snmp.get_arp_entry(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "upsert_arp_entry/1" do
|
|
test "creates new ARP entry when none exists", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
attrs = %{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
entry_type: "dynamic",
|
|
if_index: 1,
|
|
last_seen_at: DateTime.utc_now()
|
|
}
|
|
|
|
{:ok, arp} = Snmp.upsert_arp_entry(attrs)
|
|
assert arp.ip_address == "192.168.1.100"
|
|
assert arp.mac_address == "aa:bb:cc:dd:ee:ff"
|
|
end
|
|
|
|
test "updates existing ARP entry when match found", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
existing =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
entry_type: "dynamic",
|
|
if_index: 1,
|
|
last_seen_at: DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
new_time = Towerops.Time.now()
|
|
|
|
attrs = %{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
entry_type: "static",
|
|
if_index: 1,
|
|
last_seen_at: new_time
|
|
}
|
|
|
|
{:ok, updated} = Snmp.upsert_arp_entry(attrs)
|
|
assert updated.id == existing.id
|
|
assert updated.entry_type == "static"
|
|
end
|
|
end
|
|
|
|
describe "delete_stale_arp_entries/2" do
|
|
test "deletes ARP entries older than cutoff", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
old_time = DateTime.add(DateTime.utc_now(), -7200, :second)
|
|
recent_time = DateTime.add(DateTime.utc_now(), -60, :second)
|
|
|
|
old_arp =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
last_seen_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
recent_arp =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "192.168.1.200",
|
|
mac_address: "11:22:33:44:55:66",
|
|
last_seen_at: recent_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
{deleted_count, _} = Snmp.delete_stale_arp_entries(device.id, cutoff)
|
|
|
|
assert deleted_count == 1
|
|
assert Snmp.get_arp_entry(old_arp.id) == nil
|
|
assert Snmp.get_arp_entry(recent_arp.id)
|
|
end
|
|
|
|
test "does not delete ARP entries from other devices", %{organization: organization} do
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Another Site",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device1} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device2} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 2",
|
|
ip_address: "192.168.1.2",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
old_time = DateTime.add(DateTime.utc_now(), -7200, :second)
|
|
|
|
arp1 =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device1.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
last_seen_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
arp2 =
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device2.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "11:22:33:44:55:66",
|
|
last_seen_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
{deleted_count, _} = Snmp.delete_stale_arp_entries(device1.id, cutoff)
|
|
|
|
assert deleted_count == 1
|
|
assert Snmp.get_arp_entry(arp1.id) == nil
|
|
assert Snmp.get_arp_entry(arp2.id)
|
|
end
|
|
end
|
|
|
|
describe "upsert_arp_entries/3" do
|
|
test "creates multiple ARP entries with interface mapping", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
arp_data = [
|
|
%{
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
if_index: 1,
|
|
entry_type: "dynamic",
|
|
last_seen_at: DateTime.utc_now()
|
|
},
|
|
%{
|
|
ip_address: "192.168.1.200",
|
|
mac_address: "11:22:33:44:55:66",
|
|
if_index: 1,
|
|
entry_type: "static",
|
|
last_seen_at: DateTime.utc_now()
|
|
}
|
|
]
|
|
|
|
{success, errors} = Snmp.upsert_arp_entries(device.id, arp_data, [interface])
|
|
assert success == 2
|
|
assert errors == 0
|
|
|
|
entries = Snmp.list_arp_entries(device.id)
|
|
assert length(entries) == 2
|
|
|
|
ips = entries |> Enum.map(& &1.ip_address) |> Enum.sort()
|
|
assert ips == ["192.168.1.100", "192.168.1.200"]
|
|
|
|
# Both should have the interface linked
|
|
assert Enum.all?(entries, &(&1.interface_id == interface.id))
|
|
end
|
|
|
|
test "handles entries without matching interface", %{device: device} do
|
|
arp_data = [
|
|
%{
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
if_index: 999,
|
|
entry_type: "dynamic",
|
|
last_seen_at: DateTime.utc_now()
|
|
}
|
|
]
|
|
|
|
{success, errors} = Snmp.upsert_arp_entries(device.id, arp_data, [])
|
|
assert success == 1
|
|
assert errors == 0
|
|
|
|
entries = Snmp.list_arp_entries(device.id)
|
|
assert length(entries) == 1
|
|
assert hd(entries).interface_id == nil
|
|
assert hd(entries).if_index == 999
|
|
end
|
|
end
|
|
|
|
describe "upsert_arp_entries/3 error tracking" do
|
|
test "returns success and error counts", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
arp_data = [
|
|
%{
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
if_index: 1,
|
|
entry_type: "dynamic",
|
|
last_seen_at: DateTime.utc_now()
|
|
},
|
|
%{
|
|
ip_address: "192.168.1.200",
|
|
mac_address: "11:22:33:44:55:66",
|
|
if_index: 1,
|
|
entry_type: "static",
|
|
last_seen_at: DateTime.utc_now()
|
|
}
|
|
]
|
|
|
|
{success, errors} = Snmp.upsert_arp_entries(device.id, arp_data, [interface])
|
|
assert success == 2
|
|
assert errors == 0
|
|
end
|
|
end
|
|
|
|
describe "upsert_mac_addresses/3 error tracking" do
|
|
test "returns success and error counts", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
mac_data = [
|
|
%{
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
port_index: 1,
|
|
vlan_id: 1,
|
|
last_seen_at: DateTime.utc_now()
|
|
},
|
|
%{
|
|
mac_address: "11:22:33:44:55:66",
|
|
port_index: 1,
|
|
vlan_id: 1,
|
|
last_seen_at: DateTime.utc_now()
|
|
}
|
|
]
|
|
|
|
{success, errors} = Snmp.upsert_mac_addresses(device.id, mac_data, [interface])
|
|
assert success == 2
|
|
assert errors == 0
|
|
end
|
|
end
|
|
|
|
describe "list_discovered_devices_for_organization/1" do
|
|
test "merges data from neighbors, ARP, and MAC tables", %{
|
|
organization: organization,
|
|
device: device,
|
|
snmp_device: snmp_device
|
|
} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create a neighbor entry
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_system_name: "discovered-switch",
|
|
remote_platform: "Cisco IOS Software",
|
|
remote_capabilities: ["bridge", "router"],
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create an ARP entry with same MAC
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
if_index: 1,
|
|
entry_type: "dynamic",
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
|
|
assert length(discovered) == 1
|
|
entry = hd(discovered)
|
|
|
|
# Should merge data from both sources
|
|
assert entry.identifier.type == :mac
|
|
assert entry.identifier.value == "aa:bb:cc:dd:ee:ff"
|
|
assert entry.hostname == "discovered-switch"
|
|
assert "192.168.1.100" in entry.ip_addresses
|
|
assert "aa:bb:cc:dd:ee:ff" in entry.mac_addresses
|
|
assert entry.source_count == 2
|
|
assert :lldp in entry.protocols_used
|
|
assert :arp in entry.protocols_used
|
|
end
|
|
|
|
test "filters out existing devices by IP address", %{
|
|
organization: organization,
|
|
device: device,
|
|
snmp_device: snmp_device
|
|
} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create neighbor with same IP as existing device
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_system_name: "test-router",
|
|
remote_address: device.ip_address,
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
|
|
# Should be filtered out
|
|
assert discovered == []
|
|
end
|
|
|
|
test "filters out existing devices by MAC address", %{
|
|
organization: organization,
|
|
device: device,
|
|
snmp_device: snmp_device
|
|
} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
if_phys_address: "aa:bb:cc:dd:ee:ff"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create neighbor with same MAC as existing interface
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_system_name: "discovered-device",
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
|
|
# Should be filtered out
|
|
assert discovered == []
|
|
end
|
|
|
|
test "infers device type from LLDP capabilities", %{
|
|
organization: organization,
|
|
device: device,
|
|
snmp_device: snmp_device
|
|
} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create neighbor with bridge capability
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_system_name: "switch-device",
|
|
remote_capabilities: ["bridge"],
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
|
|
assert length(discovered) == 1
|
|
assert hd(discovered).device_type == :switch
|
|
end
|
|
|
|
test "infers device type from platform description", %{
|
|
organization: organization,
|
|
device: device,
|
|
snmp_device: snmp_device
|
|
} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create neighbor with Cisco IOS description
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_system_name: "router-device",
|
|
remote_platform: "Cisco IOS Software, Version 15.0",
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
|
|
assert length(discovered) == 1
|
|
assert hd(discovered).device_type == :router
|
|
end
|
|
|
|
test "prefers MAC address as primary identifier", %{
|
|
organization: organization,
|
|
device: device,
|
|
snmp_device: snmp_device
|
|
} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create neighbor with both MAC and IP
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:ff",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_system_name: "test-device",
|
|
remote_address: "192.168.1.100",
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
|
|
assert length(discovered) == 1
|
|
entry = hd(discovered)
|
|
|
|
# Should prefer MAC as identifier
|
|
assert entry.identifier.type == :mac
|
|
assert entry.identifier.value == "aa:bb:cc:dd:ee:ff"
|
|
end
|
|
|
|
test "returns empty list when no discovery data exists", %{organization: organization} do
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
assert discovered == []
|
|
end
|
|
|
|
test "sorts by source count and last seen", %{organization: organization, device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create one neighbor with single source (older)
|
|
old_time = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "11:11:11:11:11:11",
|
|
remote_port_id: "Gi1/0/1",
|
|
remote_system_name: "old-device",
|
|
protocol: "lldp",
|
|
last_discovered_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create another neighbor with single source (newer)
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "22:22:22:22:22:22",
|
|
remote_port_id: "Gi1/0/2",
|
|
remote_system_name: "new-device",
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Create a device with multiple sources
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(%{
|
|
device_id: device.id,
|
|
snmp_device_id: snmp_device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "33:33:33:33:33:33",
|
|
remote_port_id: "Gi1/0/3",
|
|
remote_system_name: "multi-source",
|
|
protocol: "lldp",
|
|
last_discovered_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(%{
|
|
device_id: device.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "33:33:33:33:33:33",
|
|
if_index: 1,
|
|
entry_type: "dynamic",
|
|
last_seen_at: old_time
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
discovered = Snmp.list_discovered_devices_for_organization(organization.id)
|
|
|
|
assert length(discovered) == 3
|
|
|
|
# First should be multi-source (source_count = 2)
|
|
assert hd(discovered).hostname == "multi-source"
|
|
assert hd(discovered).source_count == 2
|
|
|
|
# Next two should be sorted by last_seen (newer first)
|
|
assert Enum.at(discovered, 1).hostname == "new-device"
|
|
assert Enum.at(discovered, 2).hostname == "old-device"
|
|
end
|
|
end
|
|
|
|
describe "concurrent upsert operations (race condition tests)" do
|
|
test "concurrent neighbor upserts don't create duplicates", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Spawn 10 concurrent workers all upserting the same neighbor
|
|
tasks =
|
|
for i <- 1..10 do
|
|
Task.async(fn ->
|
|
Snmp.upsert_neighbor(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "00:11:22:33:44:55",
|
|
protocol: "lldp",
|
|
remote_system_name: "neighbor-#{i}",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
end)
|
|
end
|
|
|
|
# Wait for all tasks to complete
|
|
results = Task.await_many(tasks, 5_000)
|
|
|
|
# All should succeed
|
|
assert Enum.all?(results, fn
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
# Should have exactly 1 neighbor, not 10
|
|
neighbors =
|
|
Repo.all(
|
|
from n in Neighbor,
|
|
where: n.interface_id == ^interface.id and n.remote_chassis_id == "00:11:22:33:44:55"
|
|
)
|
|
|
|
assert length(neighbors) == 1
|
|
|
|
# The final name should be one of the concurrent writes (non-deterministic but valid)
|
|
neighbor = hd(neighbors)
|
|
assert String.starts_with?(neighbor.remote_system_name, "neighbor-")
|
|
end
|
|
|
|
test "concurrent ARP entry upserts don't create duplicates", %{device: device} do
|
|
# Spawn 10 concurrent workers all upserting the same ARP entry
|
|
tasks =
|
|
for _i <- 1..10 do
|
|
Task.async(fn ->
|
|
Snmp.upsert_arp_entry(%{
|
|
device_id: device.id,
|
|
ip_address: "192.168.1.100",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
entry_type: "dynamic",
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
end)
|
|
end
|
|
|
|
# Wait for all tasks to complete
|
|
results = Task.await_many(tasks, 5_000)
|
|
|
|
# All should succeed
|
|
assert Enum.all?(results, fn
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
# Should have exactly 1 ARP entry, not 10
|
|
arp_entries =
|
|
Repo.all(
|
|
from a in ArpEntry,
|
|
where:
|
|
a.device_id == ^device.id and a.ip_address == "192.168.1.100" and
|
|
a.mac_address == "aa:bb:cc:dd:ee:ff"
|
|
)
|
|
|
|
assert length(arp_entries) == 1
|
|
end
|
|
|
|
test "concurrent MAC address upserts don't create duplicates", %{device: device} do
|
|
# Spawn 10 concurrent workers all upserting the same MAC address
|
|
tasks =
|
|
for i <- 1..10 do
|
|
Task.async(fn ->
|
|
Snmp.upsert_mac_address(%{
|
|
device_id: device.id,
|
|
mac_address: "11:22:33:44:55:66",
|
|
vlan_id: 100,
|
|
port_index: i,
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
end)
|
|
end
|
|
|
|
# Wait for all tasks to complete
|
|
results = Task.await_many(tasks, 5_000)
|
|
|
|
# All should succeed
|
|
assert Enum.all?(results, fn
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
# Should have exactly 1 MAC address, not 10
|
|
mac_addresses =
|
|
Repo.all(
|
|
from m in MacAddress,
|
|
where: m.device_id == ^device.id and m.mac_address == "11:22:33:44:55:66" and m.vlan_id == 100
|
|
)
|
|
|
|
assert length(mac_addresses) == 1
|
|
|
|
# The final port_index should be one of the concurrent writes
|
|
mac = hd(mac_addresses)
|
|
assert mac.port_index in 1..10
|
|
end
|
|
|
|
test "concurrent MAC address upserts with nil vlan_id don't create duplicates", %{device: device} do
|
|
# Spawn 10 concurrent workers all upserting the same MAC address with nil vlan_id
|
|
tasks =
|
|
for i <- 1..10 do
|
|
Task.async(fn ->
|
|
Snmp.upsert_mac_address(%{
|
|
device_id: device.id,
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
vlan_id: nil,
|
|
port_index: i,
|
|
last_seen_at: DateTime.utc_now()
|
|
})
|
|
end)
|
|
end
|
|
|
|
# Wait for all tasks to complete
|
|
results = Task.await_many(tasks, 5_000)
|
|
|
|
# All should succeed
|
|
assert Enum.all?(results, fn
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
# Should have exactly 1 MAC address, not 10
|
|
mac_addresses =
|
|
Repo.all(
|
|
from m in MacAddress,
|
|
where: m.device_id == ^device.id and m.mac_address == "aa:bb:cc:dd:ee:ff" and is_nil(m.vlan_id)
|
|
)
|
|
|
|
assert length(mac_addresses) == 1
|
|
end
|
|
|
|
test "concurrent neighbor upserts with nil chassis_id handled correctly", %{
|
|
device: device,
|
|
snmp_device: snmp_device
|
|
} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Spawn 10 concurrent workers all upserting neighbors with nil chassis_id
|
|
tasks =
|
|
for i <- 1..10 do
|
|
Task.async(fn ->
|
|
Snmp.upsert_neighbor(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: nil,
|
|
protocol: "cdp",
|
|
remote_system_name: "neighbor-#{i}",
|
|
last_discovered_at: DateTime.utc_now()
|
|
})
|
|
end)
|
|
end
|
|
|
|
# Wait for all tasks to complete
|
|
results = Task.await_many(tasks, 5_000)
|
|
|
|
# All should succeed
|
|
assert Enum.all?(results, fn
|
|
{:ok, _} -> true
|
|
_ -> false
|
|
end)
|
|
|
|
# Should have exactly 1 neighbor with nil chassis_id
|
|
neighbors =
|
|
Repo.all(
|
|
from n in Neighbor,
|
|
where: n.interface_id == ^interface.id and is_nil(n.remote_chassis_id) and n.protocol == "cdp"
|
|
)
|
|
|
|
assert length(neighbors) == 1
|
|
end
|
|
end
|
|
|
|
describe "delete_stale_and_upsert_neighbors/3" do
|
|
test "atomically deletes stale and upserts new neighbors", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{snmp_device_id: snmp_device.id, if_index: 1, if_name: "eth0"})
|
|
|> Repo.insert!()
|
|
|
|
cutoff = DateTime.add(DateTime.utc_now(), -5, :minute)
|
|
|
|
# Create a stale neighbor (old timestamp)
|
|
{:ok, _stale} =
|
|
Snmp.upsert_neighbor(%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "stale-chassis",
|
|
remote_port_id: "ge0/0/1",
|
|
remote_system_name: "stale-switch",
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.add(DateTime.utc_now(), -10, :minute)
|
|
})
|
|
|
|
# New neighbors to upsert
|
|
new_neighbors = [
|
|
%{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
remote_chassis_id: "new-chassis",
|
|
remote_port_id: "ge0/0/2",
|
|
remote_system_name: "new-switch",
|
|
protocol: "lldp",
|
|
last_discovered_at: DateTime.utc_now()
|
|
}
|
|
]
|
|
|
|
assert {:ok, _} = Snmp.delete_stale_and_upsert_neighbors(device.id, new_neighbors, cutoff)
|
|
|
|
remaining = Repo.all(from n in Neighbor, where: n.device_id == ^device.id)
|
|
assert length(remaining) == 1
|
|
assert hd(remaining).remote_chassis_id == "new-chassis"
|
|
end
|
|
end
|
|
|
|
describe "delete_stale_and_upsert_arp_entries/4" do
|
|
test "atomically deletes stale and upserts new ARP entries", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{snmp_device_id: snmp_device.id, if_index: 1, if_name: "eth0"})
|
|
|> Repo.insert!()
|
|
|
|
now = Towerops.Time.now()
|
|
cutoff = DateTime.add(now, -5, :minute)
|
|
|
|
# Create a stale ARP entry
|
|
Repo.insert!(%ArpEntry{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
ip_address: "10.0.0.99",
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
last_seen_at: DateTime.add(now, -10, :minute)
|
|
})
|
|
|
|
# New entries to upsert
|
|
new_entries = [
|
|
%{
|
|
ip_address: "10.0.0.100",
|
|
mac_address: "11:22:33:44:55:66",
|
|
if_index: interface.if_index,
|
|
last_seen_at: now
|
|
}
|
|
]
|
|
|
|
assert {:ok, _} =
|
|
Snmp.delete_stale_and_upsert_arp_entries(
|
|
device.id,
|
|
new_entries,
|
|
[interface],
|
|
cutoff
|
|
)
|
|
|
|
remaining = Repo.all(from a in ArpEntry, where: a.device_id == ^device.id)
|
|
assert length(remaining) == 1
|
|
assert hd(remaining).ip_address == "10.0.0.100"
|
|
end
|
|
end
|
|
|
|
describe "delete_stale_and_upsert_mac_addresses/4" do
|
|
test "atomically deletes stale and upserts new MAC addresses", %{device: device, snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{snmp_device_id: snmp_device.id, if_index: 1, if_name: "eth0"})
|
|
|> Repo.insert!()
|
|
|
|
now = Towerops.Time.now()
|
|
cutoff = DateTime.add(now, -5, :minute)
|
|
|
|
# Create a stale MAC entry
|
|
Repo.insert!(%MacAddress{
|
|
device_id: device.id,
|
|
interface_id: interface.id,
|
|
mac_address: "aa:bb:cc:dd:ee:ff",
|
|
vlan_id: 1,
|
|
last_seen_at: DateTime.add(now, -10, :minute)
|
|
})
|
|
|
|
# New entries to upsert
|
|
new_entries = [
|
|
%{
|
|
mac_address: "11:22:33:44:55:66",
|
|
port_index: interface.if_index,
|
|
vlan_id: 1,
|
|
last_seen_at: now
|
|
}
|
|
]
|
|
|
|
assert {:ok, _} =
|
|
Snmp.delete_stale_and_upsert_mac_addresses(
|
|
device.id,
|
|
new_entries,
|
|
[interface],
|
|
cutoff
|
|
)
|
|
|
|
remaining = Repo.all(from m in MacAddress, where: m.device_id == ^device.id)
|
|
assert length(remaining) == 1
|
|
assert hd(remaining).mac_address == "11:22:33:44:55:66"
|
|
end
|
|
end
|
|
|
|
describe "create_checks_from_discovery/2" do
|
|
test "creates checks for sensors", %{device: device, snmp_device: snmp_device} do
|
|
# Create a sensor to discover
|
|
sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_descr: "CPU Temperature",
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: ".1.3.6.1.4.1.9.9.13.1.3.1.3.1",
|
|
sensor_unit: "°C",
|
|
sensor_divisor: 1
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Run check creation
|
|
assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
|
|
assert counts.sensors == 1
|
|
|
|
# Verify check was created (auto ping check + sensor check)
|
|
checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
|
|
check = Enum.find(checks, &(&1.check_type == "snmp_sensor"))
|
|
assert check
|
|
assert check.name == "CPU Temperature"
|
|
assert check.check_type == "snmp_sensor"
|
|
assert check.source_type == "auto_discovery"
|
|
assert check.source_id == sensor.id
|
|
assert check.device_id == device.id
|
|
assert check.enabled == true
|
|
assert check.interval_seconds == 60
|
|
|
|
# Verify config contains sensor metadata
|
|
assert check.config["sensor_type"] == "temperature"
|
|
assert check.config["sensor_oid"] == ".1.3.6.1.4.1.9.9.13.1.3.1.3.1"
|
|
end
|
|
|
|
test "creates checks for interfaces", %{device: device, snmp_device: snmp_device} do
|
|
# Create an interface
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_name: "Ethernet0",
|
|
if_type: 6,
|
|
if_speed: 1_000_000_000,
|
|
if_oper_status: "up"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
|
|
assert counts.interfaces == 1
|
|
|
|
checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
|
|
check = Enum.find(checks, &(&1.check_type == "snmp_interface"))
|
|
assert check
|
|
assert check.name == "Ethernet0 Status"
|
|
assert check.check_type == "snmp_interface"
|
|
assert check.source_type == "auto_discovery"
|
|
assert check.source_id == interface.id
|
|
assert check.config["if_index"] == 1
|
|
assert check.config["if_descr"] == "eth0"
|
|
end
|
|
|
|
test "creates checks for processors", %{device: device, snmp_device: snmp_device} do
|
|
# Create a processor
|
|
processor =
|
|
%Processor{}
|
|
|> Processor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
processor_index: "1",
|
|
description: "CPU 1",
|
|
processor_type: "hr_processor"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
|
|
assert counts.processors == 1
|
|
|
|
checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
|
|
check = Enum.find(checks, &(&1.check_type == "snmp_processor"))
|
|
assert check
|
|
assert check.name == "CPU 1"
|
|
assert check.check_type == "snmp_processor"
|
|
assert check.source_id == processor.id
|
|
end
|
|
|
|
test "creates checks for storage", %{device: device, snmp_device: snmp_device} do
|
|
# Create storage
|
|
storage =
|
|
%Storage{}
|
|
|> Storage.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
storage_index: 1,
|
|
description: "/ (root)",
|
|
storage_type: "fixed_disk",
|
|
total_bytes: 100_000_000,
|
|
used_bytes: 50_000_000
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
|
|
assert counts.storage == 1
|
|
|
|
checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
|
|
check = Enum.find(checks, &(&1.check_type == "snmp_storage"))
|
|
assert check
|
|
assert check.name == "/ (root) Usage"
|
|
assert check.check_type == "snmp_storage"
|
|
assert check.source_id == storage.id
|
|
end
|
|
|
|
test "creates multiple checks for different types", %{device: device, snmp_device: snmp_device} do
|
|
# Create multiple items
|
|
_sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_type: "temperature",
|
|
sensor_descr: "Temperature 1",
|
|
sensor_index: "1",
|
|
sensor_oid: ".1.2.3.4",
|
|
sensor_unit: "°C",
|
|
sensor_divisor: 1
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
_interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_descr: "eth0",
|
|
if_oper_status: "up"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
|
|
assert counts.sensors == 1
|
|
assert counts.interfaces == 1
|
|
assert counts.processors == 0
|
|
assert counts.storage == 0
|
|
|
|
checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
|
|
snmp_checks = Enum.reject(checks, &(&1.check_type == "ping"))
|
|
assert length(snmp_checks) == 2
|
|
|
|
check_types = snmp_checks |> Enum.map(& &1.check_type) |> Enum.sort()
|
|
assert check_types == ["snmp_interface", "snmp_sensor"]
|
|
end
|
|
|
|
test "schedules checks for execution", %{device: device, snmp_device: snmp_device} do
|
|
_sensor =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
sensor_descr: "Test Sensor",
|
|
sensor_type: "temperature",
|
|
sensor_index: "1",
|
|
sensor_oid: ".1.2.3.4",
|
|
sensor_unit: "°C",
|
|
sensor_divisor: 1
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
# Clear pre-existing jobs (e.g., from auto ping check creation)
|
|
Repo.delete_all(from(j in Oban.Job, where: j.worker == "Towerops.Workers.CheckExecutorWorker"))
|
|
|
|
assert {:ok, _counts} = Snmp.create_checks_from_discovery(device, snmp_device)
|
|
|
|
# Verify Oban job was scheduled for the discovered sensor check
|
|
jobs = Repo.all(from(j in Oban.Job, where: j.worker == "Towerops.Workers.CheckExecutorWorker"))
|
|
assert length(jobs) == 1
|
|
|
|
job = hd(jobs)
|
|
assert job.worker == "Towerops.Workers.CheckExecutorWorker"
|
|
assert job.queue == "check_executors"
|
|
end
|
|
end
|
|
|
|
describe "set_manual_capacity/2" do
|
|
test "sets manual capacity on an interface", %{snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{snmp_device_id: snmp_device.id, if_index: 1, if_name: "eth0"})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, updated} = Snmp.set_manual_capacity(interface.id, 300_000_000)
|
|
assert updated.configured_capacity_bps == 300_000_000
|
|
assert updated.capacity_source == "manual"
|
|
end
|
|
|
|
test "returns error for non-existent interface" do
|
|
assert {:error, :not_found} = Snmp.set_manual_capacity(Ecto.UUID.generate(), 100_000)
|
|
end
|
|
end
|
|
|
|
describe "clear_manual_capacity/1" do
|
|
test "clears capacity fields on an interface", %{snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
configured_capacity_bps: 300_000_000,
|
|
capacity_source: "manual"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
{:ok, updated} = Snmp.clear_manual_capacity(interface.id)
|
|
assert is_nil(updated.configured_capacity_bps)
|
|
assert is_nil(updated.capacity_source)
|
|
end
|
|
end
|
|
|
|
describe "create_wireless_client_readings_batch/1" do
|
|
setup %{device: device, organization: organization} do
|
|
# Create wireless client
|
|
wireless_client =
|
|
%WirelessClient{}
|
|
|> WirelessClient.changeset(%{
|
|
device_id: device.id,
|
|
organization_id: organization.id,
|
|
mac_address: "AA:BB:CC:DD:EE:FF",
|
|
ip_address: "10.0.1.100",
|
|
signal_strength: -65,
|
|
snr: 25,
|
|
distance: 1000,
|
|
tx_rate: 100_000,
|
|
rx_rate: 50_000,
|
|
uptime_seconds: 3600,
|
|
last_seen_at: Towerops.Time.now()
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%{wireless_client: wireless_client}
|
|
end
|
|
|
|
test "inserts batch of wireless client readings", %{
|
|
snmp_device: snmp_device,
|
|
organization: organization,
|
|
wireless_client: wireless_client
|
|
} do
|
|
now = Towerops.Time.now()
|
|
|
|
entries = [
|
|
%{
|
|
device_id: snmp_device.id,
|
|
wireless_client_id: wireless_client.id,
|
|
organization_id: organization.id,
|
|
mac_address: wireless_client.mac_address,
|
|
ip_address: wireless_client.ip_address,
|
|
signal_strength: -65,
|
|
snr: 25,
|
|
distance: 1000,
|
|
tx_rate: 100_000,
|
|
rx_rate: 50_000,
|
|
uptime_seconds: 3600,
|
|
checked_at: now
|
|
},
|
|
%{
|
|
device_id: snmp_device.id,
|
|
wireless_client_id: wireless_client.id,
|
|
organization_id: organization.id,
|
|
mac_address: wireless_client.mac_address,
|
|
ip_address: wireless_client.ip_address,
|
|
signal_strength: -70,
|
|
snr: 20,
|
|
distance: 1200,
|
|
tx_rate: 80_000,
|
|
rx_rate: 40_000,
|
|
uptime_seconds: 3700,
|
|
checked_at: DateTime.add(now, -60, :second)
|
|
}
|
|
]
|
|
|
|
{count, nil} = Snmp.create_wireless_client_readings_batch(entries)
|
|
assert count == 2
|
|
|
|
# Verify readings were inserted
|
|
readings =
|
|
Repo.all(
|
|
from r in WirelessClientReading,
|
|
where: r.wireless_client_id == ^wireless_client.id,
|
|
order_by: [desc: r.checked_at]
|
|
)
|
|
|
|
assert length(readings) == 2
|
|
[reading1, reading2] = readings
|
|
|
|
# Verify first reading (most recent)
|
|
assert reading1.device_id == snmp_device.id
|
|
assert reading1.wireless_client_id == wireless_client.id
|
|
assert reading1.organization_id == organization.id
|
|
assert reading1.mac_address == wireless_client.mac_address
|
|
assert reading1.signal_strength == -65
|
|
assert reading1.snr == 25
|
|
assert reading1.checked_at == now
|
|
|
|
# Verify second reading (older)
|
|
assert reading2.signal_strength == -70
|
|
assert reading2.snr == 20
|
|
assert DateTime.diff(reading1.checked_at, reading2.checked_at) == 60
|
|
end
|
|
|
|
test "handles empty list", _context do
|
|
{count, nil} = Snmp.create_wireless_client_readings_batch([])
|
|
assert count == 0
|
|
end
|
|
|
|
test "generates UUIDs and timestamps automatically", %{
|
|
snmp_device: snmp_device,
|
|
organization: organization,
|
|
wireless_client: wireless_client
|
|
} do
|
|
now = Towerops.Time.now()
|
|
|
|
entries = [
|
|
%{
|
|
device_id: snmp_device.id,
|
|
wireless_client_id: wireless_client.id,
|
|
organization_id: organization.id,
|
|
mac_address: wireless_client.mac_address,
|
|
signal_strength: -60,
|
|
checked_at: now
|
|
}
|
|
]
|
|
|
|
{count, nil} = Snmp.create_wireless_client_readings_batch(entries)
|
|
assert count == 1
|
|
|
|
reading = Repo.one!(WirelessClientReading)
|
|
assert reading.id
|
|
assert reading.inserted_at
|
|
end
|
|
|
|
test "handles nil metric values", %{
|
|
snmp_device: snmp_device,
|
|
organization: organization,
|
|
wireless_client: wireless_client
|
|
} do
|
|
now = Towerops.Time.now()
|
|
|
|
entries = [
|
|
%{
|
|
device_id: snmp_device.id,
|
|
wireless_client_id: wireless_client.id,
|
|
organization_id: organization.id,
|
|
mac_address: wireless_client.mac_address,
|
|
signal_strength: nil,
|
|
snr: nil,
|
|
distance: nil,
|
|
tx_rate: nil,
|
|
rx_rate: nil,
|
|
uptime_seconds: nil,
|
|
checked_at: now
|
|
}
|
|
]
|
|
|
|
{count, nil} = Snmp.create_wireless_client_readings_batch(entries)
|
|
assert count == 1
|
|
|
|
reading = Repo.one!(WirelessClientReading)
|
|
assert is_nil(reading.signal_strength)
|
|
assert is_nil(reading.snr)
|
|
assert is_nil(reading.distance)
|
|
end
|
|
end
|
|
end
|