Add hierarchical SNMP configuration with org/site/equipment fallback
Implements a hierarchical SNMP community configuration system where settings can be defined at the organization level (global default), overridden at the site level, or overridden at the equipment level. Changes: - Migration: Add snmp_version and snmp_community to organizations and sites - Organization schema: Add SNMP fields with default version "2c" - Site schema: Add SNMP fields that override organization defaults - Equipment context: Add get_snmp_config/1 with hierarchical fallback logic - Discovery: Use Equipment.get_snmp_config instead of equipment.snmp_community - PollerWorker: Use Equipment.get_snmp_config instead of equipment.snmp_community The fallback order is: 1. Equipment-level (highest priority) 2. Site-level 3. Organization-level (default) All 785 tests passing.
This commit is contained in:
parent
de002f9903
commit
ba1e8933e4
6 changed files with 132 additions and 15 deletions
|
|
@ -137,6 +137,76 @@ defmodule Towerops.Equipment do
|
|||
Repo.one!(from(e in EquipmentSchema, where: e.id == ^equipment_id, where: e.site_id == ^site_id, preload: [:site]))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets SNMP configuration for equipment with hierarchical fallback.
|
||||
|
||||
Falls back in this order:
|
||||
1. Equipment-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 (:equipment, :site, or :organization)
|
||||
"""
|
||||
def get_snmp_config(equipment_id) when is_binary(equipment_id) do
|
||||
equipment =
|
||||
EquipmentSchema
|
||||
|> Repo.get!(equipment_id)
|
||||
|> Repo.preload(site: :organization)
|
||||
|
||||
get_snmp_config(equipment)
|
||||
end
|
||||
|
||||
def get_snmp_config(%EquipmentSchema{} = equipment) do
|
||||
# Ensure associations are loaded
|
||||
equipment =
|
||||
if Ecto.assoc_loaded?(equipment.site) do
|
||||
if Ecto.assoc_loaded?(equipment.site.organization) do
|
||||
equipment
|
||||
else
|
||||
Repo.preload(equipment, site: :organization)
|
||||
end
|
||||
else
|
||||
Repo.preload(equipment, site: :organization)
|
||||
end
|
||||
|
||||
cond do
|
||||
# Equipment-level override
|
||||
equipment.snmp_community != nil ->
|
||||
%{
|
||||
version: equipment.snmp_version || "2c",
|
||||
community: equipment.snmp_community,
|
||||
source: :equipment
|
||||
}
|
||||
|
||||
# Site-level override
|
||||
equipment.site.snmp_community != nil ->
|
||||
%{
|
||||
version: equipment.site.snmp_version || "2c",
|
||||
community: equipment.site.snmp_community,
|
||||
source: :site
|
||||
}
|
||||
|
||||
# Organization-level default
|
||||
equipment.site.organization.snmp_community != nil ->
|
||||
%{
|
||||
version: equipment.site.organization.snmp_version || "2c",
|
||||
community: equipment.site.organization.snmp_community,
|
||||
source: :organization
|
||||
}
|
||||
|
||||
# No SNMP config set at any level
|
||||
true ->
|
||||
%{
|
||||
version: "2c",
|
||||
community: nil,
|
||||
source: :default
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates equipment.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ defmodule Towerops.Organizations.Organization do
|
|||
field :name, :string
|
||||
field :slug, :string
|
||||
|
||||
# SNMP configuration (global default for all sites/equipment)
|
||||
field :snmp_version, :string, default: "2c"
|
||||
field :snmp_community, :string
|
||||
|
||||
belongs_to :default_agent_token, AgentToken
|
||||
|
||||
has_many :memberships, Membership
|
||||
|
|
@ -28,6 +32,8 @@ defmodule Towerops.Organizations.Organization do
|
|||
id: Ecto.UUID.t(),
|
||||
name: String.t(),
|
||||
slug: String.t(),
|
||||
snmp_version: String.t() | nil,
|
||||
snmp_community: String.t() | nil,
|
||||
default_agent_token_id: Ecto.UUID.t() | nil,
|
||||
default_agent_token: NotLoaded.t() | AgentToken.t() | nil,
|
||||
memberships: NotLoaded.t() | [Membership.t()],
|
||||
|
|
@ -40,9 +46,10 @@ defmodule Towerops.Organizations.Organization do
|
|||
@doc false
|
||||
def changeset(organization, attrs) do
|
||||
organization
|
||||
|> cast(attrs, [:name, :default_agent_token_id])
|
||||
|> cast(attrs, [:name, :default_agent_token_id, :snmp_version, :snmp_community])
|
||||
|> validate_required([:name])
|
||||
|> validate_length(:name, min: 2, max: 100)
|
||||
|> validate_inclusion(:snmp_version, ["1", "2c", "3"], message: "must be 1, 2c, or 3")
|
||||
|> generate_slug()
|
||||
|> validate_required([:slug])
|
||||
|> unique_constraint(:slug)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ defmodule Towerops.Sites.Site do
|
|||
field :description, :string
|
||||
field :location, :string
|
||||
|
||||
# SNMP configuration (overrides organization default)
|
||||
field :snmp_version, :string
|
||||
field :snmp_community, :string
|
||||
|
||||
belongs_to :organization, Organization
|
||||
belongs_to :agent_token, AgentToken
|
||||
belongs_to :parent_site, Site
|
||||
|
|
@ -31,6 +35,8 @@ defmodule Towerops.Sites.Site do
|
|||
name: String.t(),
|
||||
description: String.t() | nil,
|
||||
location: String.t() | nil,
|
||||
snmp_version: String.t() | nil,
|
||||
snmp_community: String.t() | nil,
|
||||
organization_id: Ecto.UUID.t(),
|
||||
organization: NotLoaded.t() | Organization.t(),
|
||||
agent_token_id: Ecto.UUID.t() | nil,
|
||||
|
|
@ -46,11 +52,21 @@ defmodule Towerops.Sites.Site do
|
|||
@doc false
|
||||
def changeset(site, attrs) do
|
||||
site
|
||||
|> cast(attrs, [:name, :description, :location, :organization_id, :agent_token_id, :parent_site_id])
|
||||
|> cast(attrs, [
|
||||
:name,
|
||||
:description,
|
||||
:location,
|
||||
:organization_id,
|
||||
:agent_token_id,
|
||||
:parent_site_id,
|
||||
:snmp_version,
|
||||
:snmp_community
|
||||
])
|
||||
|> validate_required([:name, :organization_id])
|
||||
|> validate_length(:name, min: 2, max: 200)
|
||||
|> validate_length(:description, max: 1000)
|
||||
|> validate_length(:location, max: 200)
|
||||
|> validate_inclusion(:snmp_version, ["1", "2c", "3"], message: "must be 1, 2c, or 3")
|
||||
|> foreign_key_constraint(:organization_id)
|
||||
|> foreign_key_constraint(:agent_token_id)
|
||||
|> foreign_key_constraint(:parent_site_id)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Equipment.Equipment
|
||||
alias Towerops.Equipment
|
||||
alias Towerops.Equipment.Equipment, as: EquipmentSchema
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.Device
|
||||
|
|
@ -93,8 +94,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
iex> discover_equipment(equipment_without_snmp)
|
||||
{:error, :snmp_not_enabled}
|
||||
"""
|
||||
@spec discover_equipment(Equipment.t()) :: {:ok, Device.t()} | {:error, term()}
|
||||
def discover_equipment(%Equipment{} = equipment) do
|
||||
@spec discover_equipment(EquipmentSchema.t()) :: {:ok, Device.t()} | {:error, term()}
|
||||
def discover_equipment(%EquipmentSchema{} = equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
Logger.info("Starting SNMP discovery for equipment: #{equipment.name} (#{equipment.ip_address})")
|
||||
|
||||
|
|
@ -138,7 +139,7 @@ defmodule Towerops.Snmp.Discovery do
|
|||
@spec discover_all(String.t()) :: {:ok, discovery_summary()}
|
||||
def discover_all(org_id) do
|
||||
equipment_list =
|
||||
Equipment
|
||||
EquipmentSchema
|
||||
|> join(:inner, [e], s in assoc(e, :site))
|
||||
|> where([e, s], s.organization_id == ^org_id and e.snmp_enabled == true)
|
||||
|> Repo.all()
|
||||
|
|
@ -170,12 +171,15 @@ defmodule Towerops.Snmp.Discovery do
|
|||
|
||||
# Private functions
|
||||
|
||||
@spec build_client_opts(Equipment.t()) :: Client.connection_opts()
|
||||
@spec build_client_opts(EquipmentSchema.t()) :: Client.connection_opts()
|
||||
defp build_client_opts(equipment) do
|
||||
# Get SNMP config with hierarchical fallback (equipment -> site -> organization)
|
||||
snmp_config = Equipment.get_snmp_config(equipment)
|
||||
|
||||
[
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
community: snmp_config.community,
|
||||
version: snmp_config.version,
|
||||
port: equipment.snmp_port || 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
|
@ -268,7 +272,7 @@ defmodule Towerops.Snmp.Discovery do
|
|||
end
|
||||
|
||||
@spec save_discovery_results(
|
||||
Equipment.t(),
|
||||
EquipmentSchema.t(),
|
||||
device_info(),
|
||||
[interface_data()],
|
||||
[sensor_data()]
|
||||
|
|
@ -288,7 +292,7 @@ defmodule Towerops.Snmp.Discovery do
|
|||
end)
|
||||
end
|
||||
|
||||
@spec upsert_device(Equipment.t(), device_info()) :: Device.t()
|
||||
@spec upsert_device(EquipmentSchema.t(), device_info()) :: Device.t()
|
||||
defp upsert_device(equipment, device_info) do
|
||||
case Repo.get_by(Device, equipment_id: equipment.id) do
|
||||
nil ->
|
||||
|
|
@ -328,8 +332,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
end)
|
||||
end
|
||||
|
||||
@spec update_equipment_discovery_time(Equipment.t()) ::
|
||||
{:ok, Equipment.t()} | {:error, Ecto.Changeset.t()}
|
||||
@spec update_equipment_discovery_time(EquipmentSchema.t()) ::
|
||||
{:ok, EquipmentSchema.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp update_equipment_discovery_time(equipment) do
|
||||
equipment
|
||||
|> Ecto.Changeset.change(last_discovery_at: DateTime.truncate(DateTime.utc_now(), :second))
|
||||
|
|
|
|||
|
|
@ -636,10 +636,13 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
end
|
||||
|
||||
defp build_client_opts(equipment) do
|
||||
# Get SNMP config with hierarchical fallback (equipment -> site -> organization)
|
||||
snmp_config = Equipment.get_snmp_config(equipment)
|
||||
|
||||
[
|
||||
ip: equipment.ip_address,
|
||||
community: equipment.snmp_community,
|
||||
version: equipment.snmp_version,
|
||||
community: snmp_config.community,
|
||||
version: snmp_config.version,
|
||||
port: equipment.snmp_port || 161,
|
||||
timeout: 5000
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
defmodule Towerops.Repo.Migrations.AddSnmpConfigToOrganizationsAndSites do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
# Add SNMP configuration to organizations (global default)
|
||||
alter table(:organizations) do
|
||||
add :snmp_version, :string, default: "2c"
|
||||
add :snmp_community, :string
|
||||
end
|
||||
|
||||
# Add SNMP configuration to sites (override org default)
|
||||
alter table(:sites) do
|
||||
add :snmp_version, :string
|
||||
add :snmp_community, :string
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue