towerops/lib/towerops/device_profiles.ex
Graham McIntire 6c6b68f8fb
fix: normalize OID format for sysObjectID matching
LibreNMS uses leading dot format (.1.3.6.1...) but SNMP returns
OIDs without leading dot (1.3.6.1...). Normalize by prepending
dot to sys_object_id before matching, and use starts_with instead
of contains for more accurate prefix matching.
2026-01-17 18:18:38 -06:00

219 lines
5.6 KiB
Elixir

defmodule Towerops.DeviceProfiles do
@moduledoc """
Context for managing device SNMP profiles.
Provides functions for querying profiles, matching devices to profiles based on
detection rules, and retrieving sensor/processor/mempool definitions.
"""
import Ecto.Query
alias Towerops.DeviceProfiles.DetectionRule
alias Towerops.DeviceProfiles.DeviceProfile
alias Towerops.DeviceProfiles.MemPoolDefinition
alias Towerops.DeviceProfiles.OSDefinition
alias Towerops.DeviceProfiles.ProcessorDefinition
alias Towerops.DeviceProfiles.SensorDefinition
alias Towerops.DeviceProfiles.SensorState
alias Towerops.Repo
@doc """
Lists all enabled device profiles.
"""
def list_profiles do
Repo.all(from p in DeviceProfile, where: p.enabled == true, order_by: [asc: p.priority])
end
@doc """
Gets a single device profile by OS name.
"""
def get_profile(os) when is_binary(os) do
Repo.get_by(DeviceProfile, os: os)
end
@doc """
Gets a profile with all associations preloaded.
"""
def get_profile_with_associations(os) when is_binary(os) do
Repo.one(
from p in DeviceProfile,
where: p.os == ^os,
preload: [
:detection_rules,
:os_definitions,
:processor_definitions,
:mempool_definitions,
sensor_definitions: [:states]
]
)
end
@doc """
Matches a device to a profile based on system information.
Takes a map with :sys_descr and :sys_object_id keys and returns the
matching profile or nil.
"""
def match_profile(system_info) do
profiles = list_profiles_with_detection_rules()
Enum.find(profiles, fn profile ->
match_detection_rules?(profile.detection_rules, system_info)
end)
end
@doc """
Lists all sensor definitions for a given profile and sensor class.
"""
def list_sensor_definitions(profile_id, sensor_class) do
Repo.all(
from s in SensorDefinition,
where: s.device_profile_id == ^profile_id and s.sensor_class == ^sensor_class,
preload: [:states]
)
end
@doc """
Creates a new device profile.
"""
def create_profile(attrs \\ %{}) do
%DeviceProfile{}
|> DeviceProfile.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a device profile.
"""
def update_profile(%DeviceProfile{} = profile, attrs) do
profile
|> DeviceProfile.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a device profile and all associated definitions.
"""
def delete_profile(%DeviceProfile{} = profile) do
Repo.delete(profile)
end
@doc """
Creates a detection rule for a profile.
"""
def create_detection_rule(attrs \\ %{}) do
%DetectionRule{}
|> DetectionRule.changeset(attrs)
|> Repo.insert()
end
@doc """
Creates a sensor definition for a profile.
"""
def create_sensor_definition(attrs \\ %{}) do
%SensorDefinition{}
|> SensorDefinition.changeset(attrs)
|> Repo.insert()
end
@doc """
Creates a sensor state for a sensor definition.
"""
def create_sensor_state(attrs \\ %{}) do
%SensorState{}
|> SensorState.changeset(attrs)
|> Repo.insert()
end
@doc """
Creates a processor definition for a profile.
"""
def create_processor_definition(attrs \\ %{}) do
%ProcessorDefinition{}
|> ProcessorDefinition.changeset(attrs)
|> Repo.insert()
end
@doc """
Creates a memory pool definition for a profile.
"""
def create_mempool_definition(attrs \\ %{}) do
%MemPoolDefinition{}
|> MemPoolDefinition.changeset(attrs)
|> Repo.insert()
end
@doc """
Creates an OS definition for a profile.
"""
def create_os_definition(attrs \\ %{}) do
%OSDefinition{}
|> OSDefinition.changeset(attrs)
|> Repo.insert()
end
# Private functions
defp list_profiles_with_detection_rules do
Repo.all(
from p in DeviceProfile,
where: p.enabled == true,
order_by: [asc: p.priority],
preload: [:detection_rules]
)
end
defp match_detection_rules?(rules, system_info) do
Enum.any?(rules, &match_rule?(&1, system_info))
end
defp match_rule?(%DetectionRule{rule_type: "sysObjectID"} = rule, system_info) do
sys_object_id = Map.get(system_info, :sys_object_id, "")
# Normalize OID - prepend dot if not present for pattern matching
# LibreNMS uses ".1.3.6.1..." format, SNMP returns "1.3.6.1..." format
normalized_oid = if String.starts_with?(sys_object_id, "."), do: sys_object_id, else: "." <> sys_object_id
cond do
rule.pattern && rule.pattern != "" ->
String.starts_with?(normalized_oid, rule.pattern)
rule.value && rule.value != "" ->
normalized_value = if String.starts_with?(rule.value, "."), do: rule.value, else: "." <> rule.value
normalized_oid == normalized_value
true ->
false
end
end
defp match_rule?(%DetectionRule{rule_type: "sysDescr"} = rule, system_info) do
sys_descr = Map.get(system_info, :sys_descr, "")
cond do
rule.pattern && rule.pattern != "" ->
# Handle regex patterns
case Regex.compile(rule.pattern) do
{:ok, regex} -> Regex.match?(regex, sys_descr)
{:error, _} -> String.contains?(sys_descr, rule.pattern)
end
rule.value && rule.value != "" ->
String.contains?(sys_descr, rule.value)
true ->
false
end
end
defp match_rule?(%DetectionRule{rule_type: "sysDescr_regex"} = rule, system_info) do
sys_descr = Map.get(system_info, :sys_descr, "")
case Regex.compile(rule.pattern || "") do
{:ok, regex} -> Regex.match?(regex, sys_descr)
{:error, _} -> false
end
end
defp match_rule?(_rule, _system_info), do: false
end