298 lines
6.7 KiB
Elixir
298 lines
6.7 KiB
Elixir
defmodule Towerops.Devices do
|
|
@moduledoc """
|
|
The Devices context.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Devices.Device, as: DeviceSchema
|
|
alias Towerops.Devices.Event
|
|
alias Towerops.Repo
|
|
|
|
@doc """
|
|
Returns the list of devices for a site.
|
|
"""
|
|
def list_site_devices(site_id) do
|
|
Repo.all(from(e in DeviceSchema, where: e.site_id == ^site_id, order_by: [asc: e.name]))
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of all devices for an organization (via sites).
|
|
|
|
Supports filtering by:
|
|
- site_id: Filter by specific site
|
|
- status: Filter by status ("up", "down", "unknown")
|
|
"""
|
|
def list_organization_devices(organization_id, filters \\ %{}) do
|
|
query =
|
|
from(e in DeviceSchema,
|
|
join: s in assoc(e, :site),
|
|
where: s.organization_id == ^organization_id,
|
|
order_by: [asc: e.name],
|
|
preload: [site: s]
|
|
)
|
|
|
|
query =
|
|
if site_id = filters["site_id"] do
|
|
where(query, [e], e.site_id == ^site_id)
|
|
else
|
|
query
|
|
end
|
|
|
|
query =
|
|
if status = filters["status"] do
|
|
where(query, [e], e.status == ^status)
|
|
else
|
|
query
|
|
end
|
|
|
|
Repo.all(query)
|
|
end
|
|
|
|
@doc """
|
|
Returns the count of devices for an organization.
|
|
"""
|
|
def count_organization_devices(organization_id) do
|
|
Repo.aggregate(
|
|
from(e in DeviceSchema,
|
|
join: s in assoc(e, :site),
|
|
where: s.organization_id == ^organization_id
|
|
),
|
|
:count
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Returns the count of devices for a site.
|
|
"""
|
|
def count_site_devices(site_id) do
|
|
Repo.aggregate(
|
|
from(e in DeviceSchema, where: e.site_id == ^site_id),
|
|
:count
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Returns the count of devices that is down for a site.
|
|
"""
|
|
def count_site_devices_down(site_id) do
|
|
Repo.aggregate(
|
|
from(e in DeviceSchema, where: e.site_id == ^site_id and e.status == "down"),
|
|
:count
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of all devices with monitoring enabled.
|
|
"""
|
|
def list_monitored_devices do
|
|
Repo.all(
|
|
from(e in DeviceSchema,
|
|
where: e.monitoring_enabled == true,
|
|
order_by: [asc: e.name]
|
|
)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of all devices with SNMP enabled.
|
|
"""
|
|
def list_snmp_enabled_devices do
|
|
Repo.all(
|
|
from(e in DeviceSchema,
|
|
where: e.snmp_enabled == true,
|
|
order_by: [asc: e.name]
|
|
)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Gets a single device.
|
|
"""
|
|
def get_device!(id) do
|
|
DeviceSchema
|
|
|> Repo.get!(id)
|
|
|> Repo.preload([:site])
|
|
end
|
|
|
|
@doc """
|
|
Gets device with full details including SNMP device, interfaces, and sensors.
|
|
"""
|
|
def get_device_with_details(id) do
|
|
DeviceSchema
|
|
|> Repo.get(id)
|
|
|> case do
|
|
nil ->
|
|
nil
|
|
|
|
device ->
|
|
Repo.preload(device, [:site, snmp_device: [:interfaces, :sensors]])
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a single device belonging to a specific site.
|
|
"""
|
|
def get_site_device!(site_id, device_id) do
|
|
Repo.one!(from(e in DeviceSchema, where: e.id == ^device_id, where: e.site_id == ^site_id, preload: [:site]))
|
|
end
|
|
|
|
@doc """
|
|
Gets SNMP configuration for device with hierarchical fallback.
|
|
|
|
Falls back in this order:
|
|
1. Device-level configuration
|
|
2. Site-level configuration
|
|
3. Organization-level configuration
|
|
|
|
Returns a map with:
|
|
- version: SNMP version ("1", "2c", or "3")
|
|
- community: SNMP community string
|
|
- source: Where the config came from (:device, :site, or :organization)
|
|
"""
|
|
def get_snmp_config(device_id) when is_binary(device_id) do
|
|
device =
|
|
DeviceSchema
|
|
|> Repo.get!(device_id)
|
|
|> Repo.preload(site: :organization)
|
|
|
|
get_snmp_config(device)
|
|
end
|
|
|
|
def get_snmp_config(%DeviceSchema{} = device) do
|
|
device = ensure_snmp_config_loaded(device)
|
|
resolve_snmp_config(device)
|
|
end
|
|
|
|
defp ensure_snmp_config_loaded(device) do
|
|
# Ensure site and organization associations are loaded
|
|
needs_preload =
|
|
!Ecto.assoc_loaded?(device.site) ||
|
|
!Ecto.assoc_loaded?(device.site.organization)
|
|
|
|
if needs_preload do
|
|
Repo.preload(device, site: :organization)
|
|
else
|
|
device
|
|
end
|
|
end
|
|
|
|
defp resolve_snmp_config(device) do
|
|
cond do
|
|
device.snmp_community != nil ->
|
|
build_snmp_config(device.snmp_version, device.snmp_community, :device)
|
|
|
|
device.site.snmp_community != nil ->
|
|
build_snmp_config(device.site.snmp_version, device.site.snmp_community, :site)
|
|
|
|
device.site.organization.snmp_community != nil ->
|
|
build_snmp_config(
|
|
device.site.organization.snmp_version,
|
|
device.site.organization.snmp_community,
|
|
:organization
|
|
)
|
|
|
|
true ->
|
|
%{version: "2c", community: nil, source: :default}
|
|
end
|
|
end
|
|
|
|
defp build_snmp_config(version, community, source) do
|
|
%{
|
|
version: version || "2c",
|
|
community: community,
|
|
source: source
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Creates device.
|
|
"""
|
|
def create_device(attrs) do
|
|
%DeviceSchema{}
|
|
|> DeviceSchema.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Updates device.
|
|
"""
|
|
def update_device(%DeviceSchema{} = device, attrs) do
|
|
device
|
|
|> DeviceSchema.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
@doc """
|
|
Deletes device.
|
|
"""
|
|
def delete_device(%DeviceSchema{} = device) do
|
|
Repo.delete(device)
|
|
end
|
|
|
|
@doc """
|
|
Returns an `%Ecto.Changeset{}` for tracking device changes.
|
|
"""
|
|
def change_device(%DeviceSchema{} = device, attrs \\ %{}) do
|
|
DeviceSchema.changeset(device, attrs)
|
|
end
|
|
|
|
@doc """
|
|
Updates the status of device.
|
|
"""
|
|
def update_device_status(%DeviceSchema{} = device, new_status) do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
changes = %{
|
|
status: new_status,
|
|
last_checked_at: now
|
|
}
|
|
|
|
changes =
|
|
if device.status == new_status do
|
|
changes
|
|
else
|
|
Map.put(changes, :last_status_change_at, now)
|
|
end
|
|
|
|
device
|
|
|> Ecto.Changeset.change(changes)
|
|
|> Repo.update()
|
|
end
|
|
|
|
@doc """
|
|
Updates the last SNMP poll timestamp for device.
|
|
Used for distributed coordination to prevent duplicate polling across pods.
|
|
"""
|
|
def update_snmp_poll_time(%DeviceSchema{} = device) do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
device
|
|
|> Ecto.Changeset.change(%{last_snmp_poll_at: now})
|
|
|> Repo.update()
|
|
end
|
|
|
|
## Events
|
|
|
|
@doc """
|
|
Creates an device event.
|
|
"""
|
|
def create_event(attrs) do
|
|
%Event{}
|
|
|> Event.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Returns the list of events for device, ordered by most recent first.
|
|
"""
|
|
def list_devices_events(device_id, limit \\ 100) do
|
|
Repo.all(
|
|
from(e in Event,
|
|
where: e.device_id == ^device_id,
|
|
order_by: [desc: e.occurred_at],
|
|
limit: ^limit
|
|
)
|
|
)
|
|
end
|
|
end
|