1972 lines
55 KiB
Elixir
1972 lines
55 KiB
Elixir
defmodule Towerops.SnmpTest do
|
|
use Towerops.DataCase
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Snmp
|
|
alias Towerops.Snmp.Device
|
|
alias Towerops.Snmp.Interface
|
|
alias Towerops.Snmp.InterfaceStat
|
|
alias Towerops.Snmp.Neighbor
|
|
alias Towerops.Snmp.Sensor
|
|
alias Towerops.Snmp.SensorReading
|
|
alias Towerops.Snmp.SnmpMock
|
|
alias Towerops.Snmp.Storage
|
|
alias Towerops.Snmp.StorageReading
|
|
|
|
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
|
|
})
|
|
|
|
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_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_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_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 = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
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
|
|
})
|
|
|
|
{:ok, device2} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 2",
|
|
ip_address: "192.168.1.2",
|
|
site_id: site.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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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 = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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
|
|
})
|
|
|
|
{:ok, device2} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 2",
|
|
ip_address: "192.168.1.2",
|
|
site_id: site.id
|
|
})
|
|
|
|
old_time = DateTime.add(DateTime.utc_now(), -7200, :second)
|
|
|
|
arp1 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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 =
|
|
%Snmp.ArpEntry{}
|
|
|> Snmp.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()
|
|
}
|
|
]
|
|
|
|
count = Snmp.upsert_arp_entries(device.id, arp_data, [interface])
|
|
assert count == 2
|
|
|
|
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()
|
|
}
|
|
]
|
|
|
|
count = Snmp.upsert_arp_entries(device.id, arp_data, [])
|
|
assert count == 1
|
|
|
|
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
|
|
end
|