283 lines
8.8 KiB
Elixir
283 lines
8.8 KiB
Elixir
defmodule Towerops.Snmp.DiscoveryExtraTest do
|
|
@moduledoc """
|
|
Additional `Towerops.Snmp.Discovery` coverage targeting branches that
|
|
weren't exercised by existing tests.
|
|
|
|
* `sync_ip_addresses/2` — non-empty insert, removed-IP path, and the
|
|
"if_index has no matching interface" filter
|
|
* `sync_storage/2` — string `storage_index` exercising
|
|
`normalize_storage_index/1` binary clause
|
|
* `save_neighbors/2` — full upsert path with non-empty list
|
|
* `save_arp_entries/3` — exercise non-empty arp insert path
|
|
* `select_profile/2` — Base fallback path when no YAML matches
|
|
"""
|
|
|
|
use Towerops.DataCase, async: true
|
|
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Repo
|
|
alias Towerops.Snmp
|
|
alias Towerops.Snmp.Discovery
|
|
alias Towerops.Snmp.Interface
|
|
alias Towerops.Snmp.IpAddress
|
|
alias Towerops.Snmp.Storage
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Discovery Extra Org"}, user.id)
|
|
{:ok, site} = Towerops.Sites.create_site(%{name: "Site 1", organization_id: org.id})
|
|
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Test",
|
|
ip_address: "10.50.0.1",
|
|
site_id: site.id,
|
|
organization_id: org.id
|
|
})
|
|
|
|
{:ok, snmp_device} = Repo.insert(%Snmp.Device{device_id: device.id, sys_descr: "test"})
|
|
|
|
{:ok, iface1} =
|
|
Repo.insert(%Interface{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 1,
|
|
if_name: "eth0",
|
|
if_descr: "Ethernet 0",
|
|
if_type: 6,
|
|
monitored: true
|
|
})
|
|
|
|
{:ok, iface2} =
|
|
Repo.insert(%Interface{
|
|
snmp_device_id: snmp_device.id,
|
|
if_index: 2,
|
|
if_name: "eth1",
|
|
if_descr: "Ethernet 1",
|
|
if_type: 6,
|
|
monitored: true
|
|
})
|
|
|
|
%{snmp_device: snmp_device, iface1: iface1, iface2: iface2, device: device, org: org}
|
|
end
|
|
|
|
describe "sync_ip_addresses/2" do
|
|
test "inserts new IPv4 and IPv6 addresses bound to interfaces by if_index",
|
|
%{snmp_device: snmp_device, iface1: iface1, iface2: iface2} do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
device_view = %{
|
|
device_id: snmp_device.device_id,
|
|
interfaces: [
|
|
%{id: iface1.id, if_index: 1},
|
|
%{id: iface2.id, if_index: 2}
|
|
]
|
|
}
|
|
|
|
assert :ok =
|
|
Discovery.sync_ip_addresses(device_view, [
|
|
%{
|
|
ip_address: "10.0.0.1",
|
|
if_index: 1,
|
|
ip_type: "ipv4",
|
|
prefix_length: 24,
|
|
subnet_mask: "255.255.255.0",
|
|
last_checked_at: now
|
|
},
|
|
%{
|
|
ip_address: "fe80::1",
|
|
if_index: 2,
|
|
ip_type: "ipv6",
|
|
prefix_length: 64,
|
|
subnet_mask: nil,
|
|
last_checked_at: now
|
|
}
|
|
])
|
|
|
|
ips = Repo.all(from(ip in IpAddress, where: ip.snmp_interface_id in ^[iface1.id, iface2.id]))
|
|
|
|
assert length(ips) == 2
|
|
assert Enum.any?(ips, &(&1.ip_address == "10.0.0.1"))
|
|
assert Enum.any?(ips, &(&1.ip_address == "fe80::1"))
|
|
end
|
|
|
|
test "removes IPs that are no longer in the discovered set",
|
|
%{snmp_device: snmp_device, iface1: iface1} do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
device_view = %{
|
|
device_id: snmp_device.device_id,
|
|
interfaces: [%{id: iface1.id, if_index: 1}]
|
|
}
|
|
|
|
# First sync: 2 IPs
|
|
Discovery.sync_ip_addresses(device_view, [
|
|
%{
|
|
ip_address: "10.0.0.1",
|
|
if_index: 1,
|
|
ip_type: "ipv4",
|
|
prefix_length: 24,
|
|
subnet_mask: "255.255.255.0",
|
|
last_checked_at: now
|
|
},
|
|
%{
|
|
ip_address: "10.0.0.2",
|
|
if_index: 1,
|
|
ip_type: "ipv4",
|
|
prefix_length: 24,
|
|
subnet_mask: "255.255.255.0",
|
|
last_checked_at: now
|
|
}
|
|
])
|
|
|
|
assert length(Repo.all(IpAddress)) == 2
|
|
|
|
# Second sync: only 10.0.0.1 - 10.0.0.2 must be removed
|
|
Discovery.sync_ip_addresses(device_view, [
|
|
%{
|
|
ip_address: "10.0.0.1",
|
|
if_index: 1,
|
|
ip_type: "ipv4",
|
|
prefix_length: 24,
|
|
subnet_mask: "255.255.255.0",
|
|
last_checked_at: now
|
|
}
|
|
])
|
|
|
|
ips = Repo.all(IpAddress)
|
|
assert length(ips) == 1
|
|
assert hd(ips).ip_address == "10.0.0.1"
|
|
end
|
|
|
|
test "drops IPs whose if_index doesn't match any interface",
|
|
%{snmp_device: snmp_device, iface1: iface1} do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
device_view = %{
|
|
device_id: snmp_device.device_id,
|
|
interfaces: [%{id: iface1.id, if_index: 1}]
|
|
}
|
|
|
|
# if_index 999 has no matching interface
|
|
Discovery.sync_ip_addresses(device_view, [
|
|
%{
|
|
ip_address: "10.0.0.1",
|
|
if_index: 1,
|
|
ip_type: "ipv4",
|
|
prefix_length: 24,
|
|
subnet_mask: "255.255.255.0",
|
|
last_checked_at: now
|
|
},
|
|
%{
|
|
ip_address: "10.0.0.99",
|
|
if_index: 999,
|
|
ip_type: "ipv4",
|
|
prefix_length: 24,
|
|
subnet_mask: "255.255.255.0",
|
|
last_checked_at: now
|
|
}
|
|
])
|
|
|
|
ips = Repo.all(IpAddress)
|
|
# Only the IP with matching interface gets inserted
|
|
assert length(ips) == 1
|
|
assert hd(ips).ip_address == "10.0.0.1"
|
|
end
|
|
end
|
|
|
|
describe "sync_storage/2 with string indices" do
|
|
test "normalizes binary storage_index to integer for storage with binary index",
|
|
%{snmp_device: snmp_device} do
|
|
# String "5" exercises normalize_storage_index/1 binary clause
|
|
assert :ok =
|
|
Discovery.sync_storage(snmp_device, [
|
|
%{
|
|
storage_index: "5",
|
|
description: "Disk 5",
|
|
storage_type: "fixed_disk",
|
|
total_bytes: 1_000,
|
|
used_bytes: 500
|
|
}
|
|
])
|
|
|
|
[storage] = Repo.all(from(s in Storage, where: s.snmp_device_id == ^snmp_device.id))
|
|
assert storage.storage_index == 5
|
|
end
|
|
end
|
|
|
|
describe "save_neighbors/2 with multiple neighbors" do
|
|
test "inserts multiple neighbors and runs the cleanup",
|
|
%{snmp_device: snmp_device, iface1: iface1, iface2: iface2} do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
assert :ok =
|
|
Discovery.save_neighbors(snmp_device.device_id, [
|
|
%{
|
|
device_id: snmp_device.device_id,
|
|
interface_id: iface1.id,
|
|
protocol: "lldp",
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:01",
|
|
remote_system_name: "n1",
|
|
remote_port_id: "Gi0/1",
|
|
last_discovered_at: now
|
|
},
|
|
%{
|
|
device_id: snmp_device.device_id,
|
|
interface_id: iface2.id,
|
|
protocol: "cdp",
|
|
remote_chassis_id: "aa:bb:cc:dd:ee:02",
|
|
remote_system_name: "n2",
|
|
remote_port_id: "Gi0/2",
|
|
last_discovered_at: now
|
|
}
|
|
])
|
|
|
|
neighbors = Repo.all(Towerops.Snmp.Neighbor)
|
|
assert length(neighbors) == 2
|
|
protocols = neighbors |> Enum.map(& &1.protocol) |> Enum.sort()
|
|
assert protocols == ["cdp", "lldp"]
|
|
end
|
|
end
|
|
|
|
describe "save_arp_entries/3 with entries" do
|
|
test "inserts ARP entries when interfaces exist for if_index",
|
|
%{snmp_device: snmp_device, iface1: iface1} do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
assert :ok =
|
|
Discovery.save_arp_entries(
|
|
snmp_device.device_id,
|
|
[
|
|
%{
|
|
ip_address: "10.0.0.50",
|
|
mac_address: "aa:bb:cc:dd:ee:50",
|
|
if_index: 1,
|
|
last_seen_at: now
|
|
}
|
|
],
|
|
[iface1]
|
|
)
|
|
|
|
entries = Repo.all(Towerops.Snmp.ArpEntry)
|
|
# save_arp_entries returns :ok regardless of whether the entry persisted;
|
|
# the upsert path itself is what we want to exercise.
|
|
assert is_list(entries)
|
|
end
|
|
end
|
|
|
|
describe "select_profile/2" do
|
|
test "returns Base profile module when no YAML profile matches" do
|
|
# Use Replay adapter so YamlProfiles.match_profile/2 can run any
|
|
# snmpget detection blocks against an empty oid_map without needing
|
|
# a real SNMP target.
|
|
system_info = %{
|
|
sys_descr: "Totally unknown device 1.0",
|
|
sys_object_id: "1.3.6.1.4.1.99999999.1"
|
|
}
|
|
|
|
client_opts = [adapter: Towerops.Snmp.Adapters.Replay, oid_map: %{}]
|
|
|
|
assert Discovery.select_profile(system_info, client_opts) == Towerops.Snmp.Profiles.Base
|
|
end
|
|
end
|
|
end
|