- Created Towerops.Schema base macro (95+ schemas updated to use it) - Created Towerops.Snmp.Reading macro (7 reading schemas consolidated) - Created Towerops.Gaiia.BaseSchema macro (4 Gaiia schemas consolidated) - Created Towerops.SyncLog shared module (UISP + Preseem merge) - Created Towerops.LogFilters (3 log filter modules merged into 1) - Created Towerops.OnCall.ChangesetHelpers (9 on-call schemas simplified) - Created API v1 ResourceController shared helpers (7 controllers) - Extracted ConnectionHelpers.format_connection_result (2 LiveViews) - Removed 5 dead config keys (scopes, mib_dirs, stripe_meter_id, etc.) - Fixed 2 pre-existing broken tests - All 13,219 tests pass, Credo clean (0 issues)
275 lines
8.1 KiB
Elixir
275 lines
8.1 KiB
Elixir
defmodule Towerops.Sites.Site do
|
|
@moduledoc """
|
|
Site schema representing physical locations within an organization.
|
|
|
|
Sites contain devices and can have site-level SNMP configuration defaults.
|
|
"""
|
|
use Towerops.Schema
|
|
|
|
alias Ecto.Association.NotLoaded
|
|
alias Towerops.Agents.AgentToken
|
|
alias Towerops.Devices.Device
|
|
alias Towerops.Encrypted.Binary
|
|
alias Towerops.Organizations.Organization
|
|
alias Towerops.Sites.Site
|
|
|
|
schema "sites" do
|
|
field :name, :string
|
|
field :description, :string
|
|
field :location, :string
|
|
field :display_order, :integer
|
|
|
|
# Geographic location fields
|
|
field :address, :string
|
|
field :latitude, :float
|
|
field :longitude, :float
|
|
|
|
# SNMP configuration (overrides organization default)
|
|
field :snmp_version, :string
|
|
field :snmp_community, :string
|
|
field :snmp_port, :integer
|
|
field :snmp_transport, :string
|
|
|
|
# SNMPv3 credentials (overrides organization default, all nullable)
|
|
field :snmpv3_security_level, :string
|
|
field :snmpv3_username, :string
|
|
field :snmpv3_auth_protocol, :string
|
|
field :snmpv3_auth_password, Binary
|
|
field :snmpv3_priv_protocol, :string
|
|
field :snmpv3_priv_password, Binary
|
|
|
|
# MikroTik API credentials (overrides organization default)
|
|
field :mikrotik_username, :string
|
|
field :mikrotik_password, Binary
|
|
field :mikrotik_port, :integer
|
|
field :mikrotik_ssh_port, :integer
|
|
field :mikrotik_use_ssl, :boolean
|
|
field :mikrotik_enabled, :boolean
|
|
|
|
belongs_to :organization, Organization
|
|
belongs_to :agent_token, AgentToken
|
|
belongs_to :parent_site, Site
|
|
has_many :child_sites, Site, foreign_key: :parent_site_id
|
|
has_many :device, Device
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@type t :: %__MODULE__{
|
|
id: Ecto.UUID.t(),
|
|
name: String.t(),
|
|
description: String.t() | nil,
|
|
location: String.t() | nil,
|
|
display_order: integer() | nil,
|
|
address: String.t() | nil,
|
|
latitude: float() | nil,
|
|
longitude: float() | nil,
|
|
snmp_version: String.t() | nil,
|
|
snmp_community: String.t() | nil,
|
|
snmp_port: integer() | nil,
|
|
snmp_transport: String.t() | nil,
|
|
snmpv3_security_level: String.t() | nil,
|
|
snmpv3_username: String.t() | nil,
|
|
snmpv3_auth_protocol: String.t() | nil,
|
|
snmpv3_auth_password: String.t() | nil,
|
|
snmpv3_priv_protocol: String.t() | nil,
|
|
snmpv3_priv_password: String.t() | nil,
|
|
mikrotik_username: String.t() | nil,
|
|
mikrotik_password: String.t() | nil,
|
|
mikrotik_port: integer() | nil,
|
|
mikrotik_ssh_port: integer() | nil,
|
|
mikrotik_use_ssl: boolean() | nil,
|
|
mikrotik_enabled: boolean() | nil,
|
|
organization_id: Ecto.UUID.t(),
|
|
organization: NotLoaded.t() | Organization.t(),
|
|
agent_token_id: Ecto.UUID.t() | nil,
|
|
agent_token: NotLoaded.t() | AgentToken.t() | nil,
|
|
parent_site_id: Ecto.UUID.t() | nil,
|
|
parent_site: NotLoaded.t() | t() | nil,
|
|
child_sites: NotLoaded.t() | [t()],
|
|
device: NotLoaded.t() | [Device.t()],
|
|
inserted_at: DateTime.t(),
|
|
updated_at: DateTime.t()
|
|
}
|
|
|
|
@doc false
|
|
def changeset(site, attrs) do
|
|
site
|
|
|> cast(attrs, [
|
|
:name,
|
|
:description,
|
|
:location,
|
|
:display_order,
|
|
:address,
|
|
:latitude,
|
|
:longitude,
|
|
:organization_id,
|
|
:agent_token_id,
|
|
:parent_site_id,
|
|
:snmp_version,
|
|
:snmp_community,
|
|
:snmp_port,
|
|
:snmp_transport,
|
|
:snmpv3_security_level,
|
|
:snmpv3_username,
|
|
:snmpv3_auth_protocol,
|
|
:snmpv3_auth_password,
|
|
:snmpv3_priv_protocol,
|
|
:snmpv3_priv_password,
|
|
:mikrotik_username,
|
|
:mikrotik_password,
|
|
:mikrotik_port,
|
|
:mikrotik_ssh_port,
|
|
:mikrotik_use_ssl,
|
|
:mikrotik_enabled
|
|
])
|
|
|> validate_required([:name, :organization_id])
|
|
|> validate_length(:name, min: 2, max: 200)
|
|
|> validate_length(:description, max: 1000)
|
|
|> validate_length(:location, max: 200)
|
|
|> validate_length(:address, max: 500)
|
|
|> validate_latitude_range()
|
|
|> validate_longitude_range()
|
|
|> validate_inclusion(:snmp_version, ["1", "2c", "3"], message: "must be 1, 2c, or 3")
|
|
|> validate_number(:snmp_port, greater_than: 0, less_than: 65_536)
|
|
|> validate_snmpv3_fields()
|
|
|> validate_mikrotik_fields()
|
|
|> foreign_key_constraint(:organization_id)
|
|
|> foreign_key_constraint(:agent_token_id)
|
|
|> foreign_key_constraint(:parent_site_id)
|
|
|> unique_constraint(:name,
|
|
name: :sites_organization_id_name_index,
|
|
message: "has already been taken for this organization"
|
|
)
|
|
|> validate_not_circular_parent()
|
|
end
|
|
|
|
defp validate_snmpv3_fields(changeset) do
|
|
snmp_version = get_field(changeset, :snmp_version)
|
|
|
|
if snmp_version == "3" do
|
|
changeset
|
|
|> derive_snmpv3_security_level()
|
|
|> validate_snmpv3_auth_required()
|
|
|> validate_snmpv3_priv_required()
|
|
|> validate_password_length(:snmpv3_auth_password, 8)
|
|
|> validate_password_length(:snmpv3_priv_password, 8)
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
defp derive_snmpv3_security_level(changeset) do
|
|
# Automatically derive security level based on what fields are present
|
|
auth_protocol = get_field(changeset, :snmpv3_auth_protocol)
|
|
priv_protocol = get_field(changeset, :snmpv3_priv_protocol)
|
|
|
|
security_level =
|
|
cond do
|
|
auth_protocol in [nil, ""] -> "noAuthNoPriv"
|
|
priv_protocol not in [nil, ""] -> "authPriv"
|
|
true -> "authNoPriv"
|
|
end
|
|
|
|
put_change(changeset, :snmpv3_security_level, security_level)
|
|
end
|
|
|
|
defp validate_snmpv3_auth_required(changeset) do
|
|
auth_protocol = get_field(changeset, :snmpv3_auth_protocol)
|
|
|
|
if auth_protocol in [nil, ""] do
|
|
changeset
|
|
else
|
|
changeset
|
|
|> validate_required([:snmpv3_auth_password],
|
|
message: "required when auth protocol is selected"
|
|
)
|
|
|> validate_inclusion(:snmpv3_auth_protocol, [
|
|
"MD5",
|
|
"SHA",
|
|
"SHA-224",
|
|
"SHA-256",
|
|
"SHA-384",
|
|
"SHA-512"
|
|
])
|
|
end
|
|
end
|
|
|
|
defp validate_snmpv3_priv_required(changeset) do
|
|
priv_protocol = get_field(changeset, :snmpv3_priv_protocol)
|
|
|
|
if priv_protocol in [nil, ""] do
|
|
changeset
|
|
else
|
|
changeset
|
|
|> validate_required([:snmpv3_priv_password],
|
|
message: "required when privacy protocol is selected"
|
|
)
|
|
|> validate_inclusion(:snmpv3_priv_protocol, ["DES", "AES", "AES-192", "AES-256", "AES-256-C"])
|
|
end
|
|
end
|
|
|
|
defp validate_password_length(changeset, field, min_length) do
|
|
case get_change(changeset, field) do
|
|
nil ->
|
|
changeset
|
|
|
|
password when byte_size(password) < min_length ->
|
|
add_error(changeset, field, "must be at least #{min_length} characters")
|
|
|
|
_ ->
|
|
changeset
|
|
end
|
|
end
|
|
|
|
defp validate_mikrotik_fields(changeset) do
|
|
changeset
|
|
|> validate_number(:mikrotik_port, greater_than: 0, less_than: 65_536)
|
|
|> validate_mikrotik_required_fields()
|
|
end
|
|
|
|
defp validate_mikrotik_required_fields(changeset) do
|
|
if get_field(changeset, :mikrotik_enabled) do
|
|
validate_required(changeset, [:mikrotik_username])
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
defp validate_not_circular_parent(changeset) do
|
|
parent_site_id = get_change(changeset, :parent_site_id)
|
|
site_id = get_field(changeset, :id)
|
|
|
|
if parent_site_id && site_id && parent_site_id == site_id do
|
|
add_error(changeset, :parent_site_id, "cannot be the same as the site itself")
|
|
else
|
|
changeset
|
|
end
|
|
end
|
|
|
|
defp validate_latitude_range(changeset) do
|
|
case get_change(changeset, :latitude) do
|
|
nil ->
|
|
changeset
|
|
|
|
latitude when latitude < -90 or latitude > 90 ->
|
|
add_error(changeset, :latitude, "must be between -90 and 90")
|
|
|
|
_ ->
|
|
changeset
|
|
end
|
|
end
|
|
|
|
defp validate_longitude_range(changeset) do
|
|
case get_change(changeset, :longitude) do
|
|
nil ->
|
|
changeset
|
|
|
|
longitude when longitude < -180 or longitude > 180 ->
|
|
add_error(changeset, :longitude, "must be between -180 and 180")
|
|
|
|
_ ->
|
|
changeset
|
|
end
|
|
end
|
|
end
|