263 lines
7.8 KiB
Elixir
263 lines
7.8 KiB
Elixir
defmodule Towerops.CapacityTest do
|
|
use Towerops.DataCase
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Capacity
|
|
alias Towerops.Snmp.Device, as: SnmpDevice
|
|
alias Towerops.Snmp.Interface
|
|
alias Towerops.Snmp.InterfaceStat
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test ISP"}, user.id)
|
|
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Tower Alpha",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "PTP Radio",
|
|
ip_address: "10.0.0.1",
|
|
snmp_enabled: true,
|
|
snmp_version: "2c",
|
|
snmp_community: "public",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
snmp_device =
|
|
%SnmpDevice{}
|
|
|> SnmpDevice.changeset(%{device_id: device.id, sys_name: "radio", sys_descr: "Cambium PTP"})
|
|
|> Repo.insert!()
|
|
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
if_speed: 1_000_000_000,
|
|
if_type: 6,
|
|
configured_capacity_bps: 300_000_000,
|
|
capacity_source: "manual"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
%{
|
|
organization: organization,
|
|
site: site,
|
|
device: device,
|
|
snmp_device: snmp_device,
|
|
interface: interface
|
|
}
|
|
end
|
|
|
|
describe "calculate_throughput/3" do
|
|
test "calculates throughput from interface stats", %{interface: interface} do
|
|
now = DateTime.utc_now()
|
|
t1 = DateTime.add(now, -120, :second)
|
|
t2 = DateTime.add(now, -60, :second)
|
|
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 1_000_000,
|
|
if_out_octets: 2_000_000,
|
|
checked_at: t1
|
|
})
|
|
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 1_500_000,
|
|
if_out_octets: 3_000_000,
|
|
checked_at: t2
|
|
})
|
|
|
|
result = Capacity.calculate_throughput(interface.id, DateTime.add(now, -180, :second))
|
|
|
|
# (500_000 bytes * 8) / 60 seconds = 66_666.67 bps
|
|
assert_in_delta result.in_bps, 66_666.67, 1.0
|
|
# (1_000_000 bytes * 8) / 60 seconds = 133_333.33 bps
|
|
assert_in_delta result.out_bps, 133_333.33, 1.0
|
|
assert_in_delta result.max_bps, 133_333.33, 1.0
|
|
end
|
|
|
|
test "clamps negative deltas to zero (counter wrap)", %{interface: interface} do
|
|
now = DateTime.utc_now()
|
|
t1 = DateTime.add(now, -120, :second)
|
|
t2 = DateTime.add(now, -60, :second)
|
|
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 5_000_000,
|
|
if_out_octets: 5_000_000,
|
|
checked_at: t1
|
|
})
|
|
|
|
# Counter wrapped - lower value than previous
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 1_000_000,
|
|
if_out_octets: 6_000_000,
|
|
checked_at: t2
|
|
})
|
|
|
|
result = Capacity.calculate_throughput(interface.id, DateTime.add(now, -180, :second))
|
|
|
|
# Negative delta clamped to 0
|
|
assert result.in_bps == 0.0
|
|
assert_in_delta result.out_bps, 133_333.33, 1.0
|
|
end
|
|
|
|
test "returns zero throughput when no stats exist", %{interface: interface} do
|
|
result = Capacity.calculate_throughput(interface.id, DateTime.add(DateTime.utc_now(), -3600, :second))
|
|
assert result.in_bps == 0.0
|
|
assert result.out_bps == 0.0
|
|
assert result.max_bps == 0.0
|
|
end
|
|
|
|
test "returns zero throughput with only one stat point", %{interface: interface} do
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 1_000_000,
|
|
if_out_octets: 2_000_000,
|
|
checked_at: DateTime.utc_now()
|
|
})
|
|
|
|
result = Capacity.calculate_throughput(interface.id, DateTime.add(DateTime.utc_now(), -3600, :second))
|
|
assert result.in_bps == 0.0
|
|
assert result.out_bps == 0.0
|
|
end
|
|
end
|
|
|
|
describe "get_utilization/1" do
|
|
test "calculates utilization percentage", %{interface: interface} do
|
|
now = DateTime.utc_now()
|
|
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
checked_at: DateTime.add(now, -120, :second)
|
|
})
|
|
|
|
# 300 Mbps capacity, sending at ~150 Mbps = 50% utilization
|
|
# 150 Mbps = 18_750_000 bytes/sec * 60 sec = 1_125_000_000 bytes
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 0,
|
|
if_out_octets: 1_125_000_000,
|
|
checked_at: DateTime.add(now, -60, :second)
|
|
})
|
|
|
|
result = Capacity.get_utilization(interface)
|
|
assert result
|
|
assert_in_delta result.utilization_pct, 50.0, 1.0
|
|
end
|
|
|
|
test "returns nil when interface has no configured capacity", %{snmp_device: snmp_device} do
|
|
interface =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 99,
|
|
if_name: "lo0"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
assert nil == Capacity.get_utilization(interface)
|
|
end
|
|
end
|
|
|
|
describe "get_site_capacity_summary/1" do
|
|
test "aggregates capacity across interfaces at a site", %{
|
|
site: site,
|
|
snmp_device: _snmp_device,
|
|
interface: interface
|
|
} do
|
|
now = DateTime.utc_now()
|
|
|
|
# Add stats for existing interface (300 Mbps capacity)
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
checked_at: DateTime.add(now, -120, :second)
|
|
})
|
|
|
|
insert_interface_stat(interface.id, %{
|
|
if_in_octets: 0,
|
|
if_out_octets: 1_125_000_000,
|
|
checked_at: DateTime.add(now, -60, :second)
|
|
})
|
|
|
|
# Add a second device with capacity at the same site
|
|
{:ok, device2} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Second Radio",
|
|
ip_address: "10.0.0.2",
|
|
snmp_enabled: true,
|
|
snmp_version: "2c",
|
|
snmp_community: "public",
|
|
site_id: site.id,
|
|
organization_id: site.organization_id
|
|
})
|
|
|
|
snmp_device2 =
|
|
%SnmpDevice{}
|
|
|> SnmpDevice.changeset(%{device_id: device2.id, sys_name: "radio2", sys_descr: "Test"})
|
|
|> Repo.insert!()
|
|
|
|
interface2 =
|
|
%Interface{}
|
|
|> Interface.changeset(%{
|
|
snmp_device_id: snmp_device2.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
configured_capacity_bps: 500_000_000,
|
|
capacity_source: "sensor"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
insert_interface_stat(interface2.id, %{
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
checked_at: DateTime.add(now, -120, :second)
|
|
})
|
|
|
|
insert_interface_stat(interface2.id, %{
|
|
if_in_octets: 0,
|
|
if_out_octets: 0,
|
|
checked_at: DateTime.add(now, -60, :second)
|
|
})
|
|
|
|
summary = Capacity.get_site_capacity_summary(site.id)
|
|
|
|
# 300M + 500M = 800M total capacity
|
|
assert summary.total_capacity_bps == 800_000_000
|
|
assert length(summary.interfaces) == 2
|
|
assert summary.utilization_pct >= 0
|
|
end
|
|
|
|
test "returns empty summary when no interfaces have capacity", %{organization: organization} do
|
|
{:ok, empty_site} =
|
|
Towerops.Sites.create_site(%{name: "Empty Site", organization_id: organization.id})
|
|
|
|
summary = Capacity.get_site_capacity_summary(empty_site.id)
|
|
assert summary.total_capacity_bps == 0
|
|
assert summary.interfaces == []
|
|
end
|
|
end
|
|
|
|
describe "get_organization_capacity_summary/1" do
|
|
test "returns per-site capacity summaries", %{organization: organization, site: site} do
|
|
summaries = Capacity.get_organization_capacity_summary(organization.id)
|
|
|
|
assert summaries != []
|
|
site_summary = Enum.find(summaries, &(&1.site_id == site.id))
|
|
assert site_summary
|
|
assert site_summary.total_capacity_bps == 300_000_000
|
|
end
|
|
end
|
|
|
|
defp insert_interface_stat(interface_id, attrs) do
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(Map.put(attrs, :interface_id, interface_id))
|
|
|> Repo.insert!()
|
|
end
|
|
end
|