Vendor modules added: - Aviat (WTM microwave radios) - Aruba (wireless controllers and APs) - CiscoWLC (Cisco wireless LAN controllers) - Teltonika (RUTOS LTE routers) - Sub10 (mmWave backhaul radios) Dialyzer fixes: - Fix unknown type Devices.t/0 in alert.ex and device.ex - Fix unmatched_return warnings across multiple files - Add :exq to PLT for Exq function detection - Remove dead code in base.ex, dynamic.ex, vendor.ex - Fix Device.t() type spec to allow nil name field Tests: 1437 tests, 0 failures Dialyzer: 0 errors Credo: no issues
144 lines
4.3 KiB
Elixir
144 lines
4.3 KiB
Elixir
defmodule Towerops.Devices.Device do
|
|
@moduledoc """
|
|
Device schema for network devices being monitored.
|
|
|
|
Devices belong to a site and organization, and can have:
|
|
- SNMP configuration (community strings, polling settings)
|
|
- Associated SNMP device record (interfaces, sensors)
|
|
- Agent assignments for remote polling
|
|
- Monitoring checks and alerts
|
|
"""
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
alias Ecto.Association.NotLoaded
|
|
alias Towerops.Agents.AgentAssignment
|
|
alias Towerops.Sites.Site
|
|
alias Towerops.Snmp.Device, as: SnmpDevice
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
@foreign_key_type :binary_id
|
|
schema "devices" do
|
|
field :name, :string
|
|
field :ip_address, :string
|
|
field :description, :string
|
|
field :status, Ecto.Enum, values: [:up, :down, :unknown], default: :unknown
|
|
field :last_checked_at, :utc_datetime
|
|
field :last_status_change_at, :utc_datetime
|
|
field :monitoring_enabled, :boolean, default: true
|
|
field :check_interval_seconds, :integer, default: 300
|
|
|
|
# SNMP fields
|
|
field :snmp_enabled, :boolean, default: false
|
|
field :snmp_version, :string, default: "2c"
|
|
field :snmp_community, :string
|
|
field :snmp_port, :integer, default: 161
|
|
field :last_discovery_at, :utc_datetime
|
|
field :last_snmp_poll_at, :utc_datetime
|
|
|
|
belongs_to :site, Site
|
|
|
|
has_one :snmp_device, SnmpDevice
|
|
has_many :agent_assignments, AgentAssignment
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@type t :: %__MODULE__{
|
|
id: Ecto.UUID.t(),
|
|
name: String.t() | nil,
|
|
ip_address: String.t(),
|
|
description: String.t() | nil,
|
|
status: :up | :down | :unknown,
|
|
last_checked_at: DateTime.t() | nil,
|
|
last_status_change_at: DateTime.t() | nil,
|
|
monitoring_enabled: boolean(),
|
|
check_interval_seconds: integer(),
|
|
snmp_enabled: boolean(),
|
|
snmp_version: String.t(),
|
|
snmp_community: String.t() | nil,
|
|
snmp_port: integer(),
|
|
last_discovery_at: DateTime.t() | nil,
|
|
last_snmp_poll_at: DateTime.t() | nil,
|
|
site_id: Ecto.UUID.t(),
|
|
site: NotLoaded.t() | Site.t(),
|
|
snmp_device: NotLoaded.t() | SnmpDevice.t() | nil,
|
|
agent_assignments: NotLoaded.t() | [AgentAssignment.t()],
|
|
inserted_at: DateTime.t(),
|
|
updated_at: DateTime.t()
|
|
}
|
|
|
|
@doc false
|
|
def changeset(device, attrs) do
|
|
device
|
|
|> cast(attrs, [
|
|
:name,
|
|
:ip_address,
|
|
:description,
|
|
:site_id,
|
|
:monitoring_enabled,
|
|
:check_interval_seconds,
|
|
:snmp_enabled,
|
|
:snmp_version,
|
|
:snmp_community,
|
|
:snmp_port
|
|
])
|
|
|> validate_required([:ip_address, :site_id])
|
|
|> validate_name()
|
|
|> validate_length(:name, min: 2, max: 200)
|
|
|> validate_length(:description, max: 1000)
|
|
|> validate_ip_address()
|
|
|> validate_number(:check_interval_seconds, greater_than: 0, less_than_or_equal_to: 3600)
|
|
|> validate_snmp()
|
|
|> foreign_key_constraint(:site_id)
|
|
end
|
|
|
|
defp validate_name(changeset) do
|
|
snmp_enabled = get_field(changeset, :snmp_enabled, false)
|
|
name = get_field(changeset, :name)
|
|
|
|
# Name is required only when SNMP is not enabled (ICMP only mode)
|
|
# When SNMP is enabled, name can be empty and will be populated from sysName during discovery
|
|
if !snmp_enabled and (is_nil(name) or name == "") do
|
|
add_error(changeset, :name, "can't be blank (required when SNMP is disabled)")
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
defp validate_ip_address(changeset) do
|
|
case get_change(changeset, :ip_address) do
|
|
nil ->
|
|
changeset
|
|
|
|
ip_string ->
|
|
case parse_ip(ip_string) do
|
|
{:ok, _} ->
|
|
changeset
|
|
|
|
{:error, _} ->
|
|
add_error(changeset, :ip_address, "must be a valid IPv4 or IPv6 address")
|
|
end
|
|
end
|
|
end
|
|
|
|
defp parse_ip(ip_string) do
|
|
ip_string
|
|
|> String.to_charlist()
|
|
|> :inet.parse_address()
|
|
end
|
|
|
|
defp validate_snmp(changeset) do
|
|
snmp_enabled = get_field(changeset, :snmp_enabled)
|
|
|
|
if snmp_enabled do
|
|
changeset
|
|
|> validate_required([:snmp_version], message: "required when SNMP is enabled")
|
|
|> validate_inclusion(:snmp_version, ["1", "2c"], message: "must be either '1' or '2c'")
|
|
|> validate_number(:snmp_port, greater_than: 0, less_than: 65_536)
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
end
|