887 lines
21 KiB
Elixir
887 lines
21 KiB
Elixir
defmodule Towerops.Snmp do
|
|
@moduledoc """
|
|
The SNMP context.
|
|
|
|
Provides the public API for SNMP discovery and monitoring functionality.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Devices.Device, as: DeviceSchema
|
|
alias Towerops.Repo
|
|
alias Towerops.Snmp.ArpEntry
|
|
alias Towerops.Snmp.Client
|
|
alias Towerops.Snmp.Device
|
|
alias Towerops.Snmp.Discovery
|
|
alias Towerops.Snmp.Interface
|
|
alias Towerops.Snmp.InterfaceStat
|
|
alias Towerops.Snmp.IpAddress
|
|
alias Towerops.Snmp.MacAddress
|
|
alias Towerops.Snmp.Neighbor
|
|
alias Towerops.Snmp.Processor
|
|
alias Towerops.Snmp.ProcessorReading
|
|
alias Towerops.Snmp.Sensor
|
|
alias Towerops.Snmp.SensorReading
|
|
alias Towerops.Snmp.StateSensor
|
|
alias Towerops.Snmp.Storage
|
|
alias Towerops.Snmp.StorageReading
|
|
alias Towerops.Snmp.Vlan
|
|
|
|
@doc """
|
|
Tests SNMP connectivity to a device.
|
|
|
|
## Examples
|
|
|
|
iex> test_connection("192.168.1.1", "public", "2c")
|
|
{:ok, "Connection successful"}
|
|
|
|
iex> test_connection("192.168.1.99", "wrong", "2c")
|
|
{:error, :timeout}
|
|
"""
|
|
def test_connection(ip, community, version, port \\ 161) do
|
|
Client.test_connection(
|
|
ip: ip,
|
|
community: community,
|
|
version: version,
|
|
port: port
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Runs SNMP discovery for a piece of device.
|
|
|
|
Discovers:
|
|
- Device information (manufacturer, model, firmware)
|
|
- Network interfaces
|
|
- Sensors (temperature, power, fans, etc.)
|
|
|
|
Updates the database with discovered data.
|
|
|
|
## Examples
|
|
|
|
iex> discover_device(device)
|
|
{:ok, %Device{}}
|
|
"""
|
|
def discover_device(%DeviceSchema{} = device) do
|
|
Discovery.discover_device(device)
|
|
end
|
|
|
|
@doc """
|
|
Runs SNMP discovery for all SNMP-enabled devices in an organization.
|
|
|
|
Returns a summary of successful and failed discoveries.
|
|
|
|
## Examples
|
|
|
|
iex> discover_all_for_org(org_id)
|
|
{:ok, %{success: 10, failed: 2, errors: [:timeout, :no_response]}}
|
|
"""
|
|
def discover_all_for_org(org_id) do
|
|
Discovery.discover_all(org_id)
|
|
end
|
|
|
|
# Device queries
|
|
|
|
@doc """
|
|
Gets the SNMP device for a piece of device.
|
|
|
|
## Examples
|
|
|
|
iex> get_device(device_id)
|
|
%Device{}
|
|
|
|
iex> get_device(nonexistent_id)
|
|
nil
|
|
"""
|
|
def get_device(device_id) do
|
|
Repo.get_by(Device, device_id: device_id)
|
|
end
|
|
|
|
@doc """
|
|
Gets the SNMP device for a device with preloaded associations.
|
|
"""
|
|
def get_device_with_associations(device_id) do
|
|
Device
|
|
|> where([d], d.device_id == ^device_id)
|
|
|> preload([:interfaces, :sensors, :state_sensors, :processors, :storage])
|
|
|> Repo.one()
|
|
end
|
|
|
|
# Interface queries
|
|
|
|
@doc """
|
|
Lists all interfaces for a device.
|
|
"""
|
|
def list_interfaces(device_id) do
|
|
Interface
|
|
|> where([i], i.snmp_device_id == ^device_id)
|
|
|> order_by([i], i.if_index)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Lists only monitored interfaces for a device.
|
|
"""
|
|
def list_monitored_interfaces(device_id) do
|
|
Interface
|
|
|> where([i], i.snmp_device_id == ^device_id and i.monitored == true)
|
|
|> order_by([i], i.if_index)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific interface.
|
|
"""
|
|
def get_interface(interface_id) do
|
|
Repo.get(Interface, interface_id)
|
|
end
|
|
|
|
@doc """
|
|
Updates an interface.
|
|
"""
|
|
def update_interface(%Interface{} = interface, attrs) do
|
|
interface
|
|
|> Interface.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
# Sensor queries
|
|
|
|
@doc """
|
|
Lists all sensors for a device.
|
|
"""
|
|
def list_sensors(device_id) do
|
|
Sensor
|
|
|> where([s], s.snmp_device_id == ^device_id)
|
|
|> order_by([s], [s.sensor_type, s.sensor_index])
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Lists only monitored sensors for a device.
|
|
"""
|
|
def list_monitored_sensors(device_id) do
|
|
Sensor
|
|
|> where([s], s.snmp_device_id == ^device_id and s.monitored == true)
|
|
|> order_by([s], [s.sensor_type, s.sensor_index])
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Lists sensors grouped by type.
|
|
"""
|
|
def list_sensors_by_type(device_id) do
|
|
sensors =
|
|
Sensor
|
|
|> where([s], s.snmp_device_id == ^device_id)
|
|
|> order_by([s], [s.sensor_type, s.sensor_index])
|
|
|> Repo.all()
|
|
|
|
Enum.group_by(sensors, & &1.sensor_type)
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific sensor.
|
|
"""
|
|
def get_sensor(sensor_id) do
|
|
Repo.get(Sensor, sensor_id)
|
|
end
|
|
|
|
@doc """
|
|
Updates a sensor.
|
|
"""
|
|
def update_sensor(%Sensor{} = sensor, attrs) do
|
|
sensor
|
|
|> Sensor.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
# State sensor queries
|
|
|
|
@doc """
|
|
Updates a state sensor.
|
|
"""
|
|
def update_state_sensor(%StateSensor{} = state_sensor, attrs) do
|
|
state_sensor
|
|
|> StateSensor.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
@doc """
|
|
Lists all state sensors for a device.
|
|
"""
|
|
def list_state_sensors(device_id) do
|
|
StateSensor
|
|
|> where([s], s.snmp_device_id == ^device_id)
|
|
|> order_by([s], s.sensor_index)
|
|
|> Repo.all()
|
|
end
|
|
|
|
# Sensor readings queries
|
|
|
|
@doc """
|
|
Gets recent sensor readings for a sensor.
|
|
|
|
## Options
|
|
|
|
- `:limit` - Maximum number of readings to return (default: 100)
|
|
- `:since` - Only return readings after this datetime
|
|
"""
|
|
def get_sensor_readings(sensor_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 100)
|
|
since = Keyword.get(opts, :since)
|
|
|
|
query =
|
|
SensorReading
|
|
|> where([r], r.sensor_id == ^sensor_id)
|
|
|> order_by([r], desc: r.checked_at)
|
|
|> limit(^limit)
|
|
|
|
query =
|
|
if since do
|
|
where(query, [r], r.checked_at >= ^since)
|
|
else
|
|
query
|
|
end
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@doc """
|
|
Gets the latest sensor reading for a sensor.
|
|
"""
|
|
def get_latest_sensor_reading(sensor_id) do
|
|
SensorReading
|
|
|> where([r], r.sensor_id == ^sensor_id)
|
|
|> order_by([r], desc: r.checked_at)
|
|
|> limit(1)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets the latest sensor readings for multiple sensors in a single query.
|
|
|
|
Returns a map of %{sensor_id => reading} for efficient batch loading.
|
|
Sensors without readings will not be present in the map.
|
|
|
|
## Examples
|
|
|
|
iex> get_latest_sensor_readings_batch([sensor_id1, sensor_id2])
|
|
%{sensor_id1 => %SensorReading{}, sensor_id2 => %SensorReading{}}
|
|
"""
|
|
def get_latest_sensor_readings_batch(sensor_ids) when is_list(sensor_ids) do
|
|
if Enum.empty?(sensor_ids) do
|
|
%{}
|
|
else
|
|
# Use DISTINCT ON to get only the latest reading per sensor
|
|
query =
|
|
from(r in SensorReading,
|
|
where: r.sensor_id in ^sensor_ids,
|
|
distinct: r.sensor_id,
|
|
order_by: [asc: r.sensor_id, desc: r.checked_at]
|
|
)
|
|
|
|
query
|
|
|> Repo.all()
|
|
|> Map.new(fn reading -> {reading.sensor_id, reading} end)
|
|
end
|
|
end
|
|
|
|
# Interface stats queries
|
|
|
|
@doc """
|
|
Gets recent interface statistics for an interface.
|
|
|
|
## Options
|
|
|
|
- `:limit` - Maximum number of stats to return (default: 100)
|
|
- `:since` - Only return stats after this datetime
|
|
"""
|
|
def get_interface_stats(interface_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 100)
|
|
since = Keyword.get(opts, :since)
|
|
|
|
query =
|
|
InterfaceStat
|
|
|> where([s], s.interface_id == ^interface_id)
|
|
|> order_by([s], desc: s.checked_at)
|
|
|> limit(^limit)
|
|
|
|
query =
|
|
if since do
|
|
where(query, [s], s.checked_at >= ^since)
|
|
else
|
|
query
|
|
end
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@doc """
|
|
Gets the latest interface stat for an interface.
|
|
"""
|
|
def get_latest_interface_stat(interface_id) do
|
|
InterfaceStat
|
|
|> where([s], s.interface_id == ^interface_id)
|
|
|> order_by([s], desc: s.checked_at)
|
|
|> limit(1)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets the latest interface stats for multiple interfaces in a single query.
|
|
|
|
Returns a map of %{interface_id => stat} for efficient batch loading.
|
|
Interfaces without stats will not be present in the map.
|
|
|
|
## Examples
|
|
|
|
iex> get_latest_interface_stats_batch([if_id1, if_id2])
|
|
%{if_id1 => %InterfaceStat{}, if_id2 => %InterfaceStat{}}
|
|
"""
|
|
def get_latest_interface_stats_batch(interface_ids) when is_list(interface_ids) do
|
|
if Enum.empty?(interface_ids) do
|
|
%{}
|
|
else
|
|
# Use DISTINCT ON to get only the latest stat per interface
|
|
query =
|
|
from(s in InterfaceStat,
|
|
where: s.interface_id in ^interface_ids,
|
|
distinct: s.interface_id,
|
|
order_by: [asc: s.interface_id, desc: s.checked_at]
|
|
)
|
|
|
|
query
|
|
|> Repo.all()
|
|
|> Map.new(fn stat -> {stat.interface_id, stat} end)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Records a new sensor reading.
|
|
"""
|
|
def create_sensor_reading(attrs) do
|
|
%SensorReading{}
|
|
|> SensorReading.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Records a new interface stat.
|
|
"""
|
|
def create_interface_stat(attrs) do
|
|
%InterfaceStat{}
|
|
|> InterfaceStat.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
# Neighbor queries
|
|
|
|
@doc """
|
|
Lists all neighbors for a piece of device.
|
|
"""
|
|
def list_neighbors(device_id) do
|
|
Neighbor
|
|
|> where([n], n.device_id == ^device_id)
|
|
|> preload(:interface)
|
|
|> order_by([n], [n.protocol, n.remote_system_name])
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Lists all neighbors for device with enriched data linking to known device.
|
|
Adds a `matched_equipment` field to each neighbor if we can identify the remote device.
|
|
"""
|
|
def list_neighbors_with_equipment(device_id) do
|
|
neighbors = list_neighbors(device_id)
|
|
|
|
# device to search within the same org
|
|
device = Towerops.Devices.get_device!(device_id)
|
|
|
|
Enum.map(neighbors, fn neighbor ->
|
|
matched_device = find_matching_equipment(neighbor, device.site.organization_id)
|
|
Map.put(neighbor, :matched_device, matched_device)
|
|
end)
|
|
end
|
|
|
|
# device in the organization
|
|
defp find_matching_equipment(neighbor, organization_id) do
|
|
# Try matching strategies in order: chassis ID, port ID, system name
|
|
try_match_by_chassis(neighbor, organization_id) ||
|
|
try_match_by_port(neighbor, organization_id) ||
|
|
try_match_by_name(neighbor, organization_id)
|
|
end
|
|
|
|
defp try_match_by_chassis(neighbor, organization_id) do
|
|
if neighbor.remote_chassis_id && mac_address?(neighbor.remote_chassis_id) do
|
|
find_device_by_mac(neighbor.remote_chassis_id, organization_id)
|
|
end
|
|
end
|
|
|
|
defp try_match_by_port(neighbor, organization_id) do
|
|
if neighbor.remote_port_id && mac_address?(neighbor.remote_port_id) do
|
|
find_device_by_mac(neighbor.remote_port_id, organization_id)
|
|
end
|
|
end
|
|
|
|
defp try_match_by_name(neighbor, organization_id) do
|
|
if neighbor.remote_system_name do
|
|
find_device_by_name(neighbor.remote_system_name, organization_id)
|
|
end
|
|
end
|
|
|
|
# Check if a string looks like a MAC address
|
|
defp mac_address?(str) when is_binary(str) do
|
|
# Match patterns like "AA:BB:CC:DD:EE:FF" or "aa-bb-cc-dd-ee-ff"
|
|
String.match?(str, ~r/^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/)
|
|
end
|
|
|
|
defp mac_address?(_), do: false
|
|
|
|
defp find_device_by_mac(mac_address, organization_id) do
|
|
# MAC address might be in format "aa:bb:cc:dd:ee:ff", "aa-bb-cc-dd-ee-ff", or "aabbccddeeff"
|
|
# Normalize to just the hex digits for comparison
|
|
normalized_mac = mac_address |> String.downcase() |> String.replace(~r/[:-]/, "")
|
|
|
|
Repo.one(
|
|
from(i in Interface,
|
|
join: d in Device,
|
|
on: i.snmp_device_id == d.id,
|
|
join: e in DeviceSchema,
|
|
on: d.device_id == e.id,
|
|
join: s in assoc(e, :site),
|
|
where: s.organization_id == ^organization_id,
|
|
where:
|
|
fragment("REPLACE(REPLACE(LOWER(?), ':', ''), '-', '')", i.if_phys_address) ==
|
|
^normalized_mac,
|
|
select: e,
|
|
limit: 1
|
|
)
|
|
)
|
|
end
|
|
|
|
defp find_device_by_name(system_name, organization_id) do
|
|
# Try exact match first, then partial match
|
|
normalized_name = String.downcase(system_name)
|
|
|
|
from(e in DeviceSchema,
|
|
join: s in assoc(e, :site),
|
|
where: s.organization_id == ^organization_id,
|
|
where: fragment("LOWER(?)", e.name) == ^normalized_name,
|
|
select: e,
|
|
limit: 1
|
|
)
|
|
|> Repo.one()
|
|
|> case do
|
|
nil ->
|
|
# Try partial match as fallback
|
|
Repo.one(
|
|
from(e in DeviceSchema,
|
|
join: s in assoc(e, :site),
|
|
where: s.organization_id == ^organization_id,
|
|
where: fragment("LOWER(?) LIKE ?", e.name, ^"%#{normalized_name}%"),
|
|
select: e,
|
|
limit: 1
|
|
)
|
|
)
|
|
|
|
device ->
|
|
device
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific neighbor.
|
|
"""
|
|
def get_neighbor(neighbor_id) do
|
|
Repo.get(Neighbor, neighbor_id)
|
|
end
|
|
|
|
@doc """
|
|
Creates or updates a neighbor record.
|
|
"""
|
|
def upsert_neighbor(attrs) do
|
|
case Repo.get_by(Neighbor,
|
|
interface_id: attrs.interface_id,
|
|
remote_chassis_id: attrs[:remote_chassis_id],
|
|
protocol: attrs.protocol
|
|
) do
|
|
nil ->
|
|
%Neighbor{}
|
|
|> Neighbor.changeset(attrs)
|
|
|> Repo.insert()
|
|
|
|
neighbor ->
|
|
neighbor
|
|
|> Neighbor.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes stale neighbors that haven't been seen since the given datetime.
|
|
"""
|
|
def delete_stale_neighbors(device_id, cutoff_datetime) do
|
|
Neighbor
|
|
|> where([n], n.device_id == ^device_id)
|
|
|> where([n], n.last_discovered_at < ^cutoff_datetime)
|
|
|> Repo.delete_all()
|
|
end
|
|
|
|
# VLAN queries
|
|
|
|
@doc """
|
|
Lists all VLANs for a device.
|
|
"""
|
|
def list_vlans(snmp_device_id) do
|
|
Vlan
|
|
|> where([v], v.snmp_device_id == ^snmp_device_id)
|
|
|> order_by([v], v.vlan_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific VLAN.
|
|
"""
|
|
def get_vlan(vlan_id) do
|
|
Repo.get(Vlan, vlan_id)
|
|
end
|
|
|
|
# IP Address queries
|
|
|
|
@doc """
|
|
Lists all IP addresses for a device (across all its interfaces).
|
|
"""
|
|
def list_ip_addresses(snmp_device_id) do
|
|
IpAddress
|
|
|> join(:inner, [ip], i in Interface, on: ip.snmp_interface_id == i.id)
|
|
|> where([ip, i], i.snmp_device_id == ^snmp_device_id)
|
|
|> preload(:snmp_interface)
|
|
|> order_by([ip], [ip.ip_type, ip.ip_address])
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Lists IP addresses for a specific interface.
|
|
"""
|
|
def list_interface_ip_addresses(interface_id) do
|
|
IpAddress
|
|
|> where([ip], ip.snmp_interface_id == ^interface_id)
|
|
|> order_by([ip], [ip.ip_type, ip.ip_address])
|
|
|> Repo.all()
|
|
end
|
|
|
|
# Processor queries
|
|
|
|
@doc """
|
|
Lists all processors for a device.
|
|
"""
|
|
def list_processors(snmp_device_id) do
|
|
Processor
|
|
|> where([p], p.snmp_device_id == ^snmp_device_id)
|
|
|> order_by([p], p.processor_index)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific processor.
|
|
"""
|
|
def get_processor(processor_id) do
|
|
Repo.get(Processor, processor_id)
|
|
end
|
|
|
|
@doc """
|
|
Updates a processor.
|
|
"""
|
|
def update_processor(processor, attrs) do
|
|
processor
|
|
|> Processor.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
# Processor reading queries
|
|
|
|
@doc """
|
|
Gets recent processor readings for a processor.
|
|
|
|
## Options
|
|
|
|
- `:limit` - Maximum number of readings to return (default: 100)
|
|
- `:since` - Only return readings after this datetime
|
|
"""
|
|
def get_processor_readings(processor_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 100)
|
|
since = Keyword.get(opts, :since)
|
|
|
|
query =
|
|
ProcessorReading
|
|
|> where([r], r.processor_id == ^processor_id)
|
|
|> order_by([r], desc: r.checked_at)
|
|
|> limit(^limit)
|
|
|
|
query =
|
|
if since do
|
|
where(query, [r], r.checked_at >= ^since)
|
|
else
|
|
query
|
|
end
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@doc """
|
|
Records a new processor reading.
|
|
"""
|
|
def create_processor_reading(attrs) do
|
|
%ProcessorReading{}
|
|
|> ProcessorReading.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
# Storage queries
|
|
|
|
@doc """
|
|
Lists all storage entries for a device.
|
|
"""
|
|
def list_storage(snmp_device_id) do
|
|
Storage
|
|
|> where([s], s.snmp_device_id == ^snmp_device_id)
|
|
|> order_by([s], s.storage_index)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific storage entry.
|
|
"""
|
|
def get_storage(storage_id) do
|
|
Repo.get(Storage, storage_id)
|
|
end
|
|
|
|
@doc """
|
|
Updates a storage entry.
|
|
"""
|
|
def update_storage(storage, attrs) do
|
|
storage
|
|
|> Storage.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
# Storage reading queries
|
|
|
|
@doc """
|
|
Gets recent storage readings for a storage entry.
|
|
|
|
## Options
|
|
|
|
- `:limit` - Maximum number of readings to return (default: 100)
|
|
- `:since` - Only return readings after this datetime
|
|
"""
|
|
def get_storage_readings(storage_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 100)
|
|
since = Keyword.get(opts, :since)
|
|
|
|
query =
|
|
StorageReading
|
|
|> where([r], r.storage_id == ^storage_id)
|
|
|> order_by([r], desc: r.checked_at)
|
|
|> limit(^limit)
|
|
|
|
query =
|
|
if since do
|
|
where(query, [r], r.checked_at >= ^since)
|
|
else
|
|
query
|
|
end
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@doc """
|
|
Gets the latest storage reading for a storage entry.
|
|
"""
|
|
def get_latest_storage_reading(storage_id) do
|
|
StorageReading
|
|
|> where([r], r.storage_id == ^storage_id)
|
|
|> order_by([r], desc: r.checked_at)
|
|
|> limit(1)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc """
|
|
Gets the latest storage readings for multiple storage entries in a single query.
|
|
|
|
Returns a map of %{storage_id => reading} for efficient batch loading.
|
|
Storage entries without readings will not be present in the map.
|
|
|
|
## Examples
|
|
|
|
iex> get_latest_storage_readings_batch([storage_id1, storage_id2])
|
|
%{storage_id1 => %StorageReading{}, storage_id2 => %StorageReading{}}
|
|
"""
|
|
def get_latest_storage_readings_batch(storage_ids) when is_list(storage_ids) do
|
|
if Enum.empty?(storage_ids) do
|
|
%{}
|
|
else
|
|
# Use DISTINCT ON to get only the latest reading per storage
|
|
query =
|
|
from(r in StorageReading,
|
|
where: r.storage_id in ^storage_ids,
|
|
distinct: r.storage_id,
|
|
order_by: [asc: r.storage_id, desc: r.checked_at]
|
|
)
|
|
|
|
query
|
|
|> Repo.all()
|
|
|> Map.new(fn reading -> {reading.storage_id, reading} end)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Records a new storage reading.
|
|
"""
|
|
def create_storage_reading(attrs) do
|
|
%StorageReading{}
|
|
|> StorageReading.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
# ARP Entry queries
|
|
|
|
@doc """
|
|
Lists all ARP entries for a device.
|
|
"""
|
|
def list_arp_entries(device_id) do
|
|
ArpEntry
|
|
|> where([a], a.device_id == ^device_id)
|
|
|> order_by([a], a.ip_address)
|
|
|> preload(:interface)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Gets a specific ARP entry.
|
|
"""
|
|
def get_arp_entry(arp_entry_id) do
|
|
Repo.get(ArpEntry, arp_entry_id)
|
|
end
|
|
|
|
@doc """
|
|
Creates or updates an ARP entry.
|
|
"""
|
|
def upsert_arp_entry(attrs) do
|
|
case Repo.get_by(ArpEntry,
|
|
device_id: attrs.device_id,
|
|
ip_address: attrs.ip_address,
|
|
mac_address: attrs.mac_address
|
|
) do
|
|
nil ->
|
|
%ArpEntry{}
|
|
|> ArpEntry.changeset(attrs)
|
|
|> Repo.insert()
|
|
|
|
arp_entry ->
|
|
arp_entry
|
|
|> ArpEntry.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes stale ARP entries that haven't been seen since the given datetime.
|
|
"""
|
|
def delete_stale_arp_entries(device_id, cutoff_datetime) do
|
|
ArpEntry
|
|
|> where([a], a.device_id == ^device_id)
|
|
|> where([a], a.last_seen_at < ^cutoff_datetime)
|
|
|> Repo.delete_all()
|
|
end
|
|
|
|
@doc """
|
|
Bulk upserts ARP entries for a device.
|
|
Returns the number of entries processed.
|
|
"""
|
|
def upsert_arp_entries(device_id, arp_entries, interfaces) do
|
|
interface_map = Map.new(interfaces, fn i -> {i.if_index, i.id} end)
|
|
|
|
Enum.each(arp_entries, fn entry ->
|
|
interface_id = Map.get(interface_map, entry.if_index)
|
|
|
|
attrs =
|
|
entry
|
|
|> Map.put(:device_id, device_id)
|
|
|> Map.put(:interface_id, interface_id)
|
|
|
|
upsert_arp_entry(attrs)
|
|
end)
|
|
|
|
length(arp_entries)
|
|
end
|
|
|
|
# MAC Address queries
|
|
|
|
@doc """
|
|
Lists all MAC address entries for a device.
|
|
"""
|
|
def list_mac_addresses(device_id) do
|
|
MacAddress
|
|
|> where([m], m.device_id == ^device_id)
|
|
|> order_by([m], [m.vlan_id, m.mac_address])
|
|
|> preload(:interface)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Creates or updates a MAC address entry.
|
|
"""
|
|
def upsert_mac_address(attrs) do
|
|
case Repo.get_by(MacAddress,
|
|
device_id: attrs.device_id,
|
|
mac_address: attrs.mac_address,
|
|
vlan_id: attrs[:vlan_id]
|
|
) do
|
|
nil ->
|
|
%MacAddress{}
|
|
|> MacAddress.changeset(attrs)
|
|
|> Repo.insert()
|
|
|
|
mac_address ->
|
|
mac_address
|
|
|> MacAddress.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Deletes stale MAC address entries that haven't been seen since the given datetime.
|
|
"""
|
|
def delete_stale_mac_addresses(device_id, cutoff_datetime) do
|
|
MacAddress
|
|
|> where([m], m.device_id == ^device_id)
|
|
|> where([m], m.last_seen_at < ^cutoff_datetime)
|
|
|> Repo.delete_all()
|
|
end
|
|
|
|
@doc """
|
|
Bulk upserts MAC address entries for a device.
|
|
Returns the number of entries processed.
|
|
"""
|
|
def upsert_mac_addresses(device_id, mac_entries, interfaces) do
|
|
# Build a map from bridge port index to interface ID
|
|
# Note: port_index from BRIDGE-MIB is not the same as if_index
|
|
# For now, we'll use a simple mapping if they're available
|
|
interface_map = Map.new(interfaces, fn i -> {i.if_index, i.id} end)
|
|
|
|
Enum.each(mac_entries, fn entry ->
|
|
interface_id = Map.get(interface_map, entry.port_index)
|
|
|
|
attrs =
|
|
entry
|
|
|> Map.put(:device_id, device_id)
|
|
|> Map.put(:interface_id, interface_id)
|
|
|
|
upsert_mac_address(attrs)
|
|
end)
|
|
|
|
length(mac_entries)
|
|
end
|
|
end
|