feat: poll actual firmware version and serial from SNMP

Enhance dynamic profile to poll OS definitions from SNMP:
- Add serial_number field to snmp_devices table
- Update Dynamic profile to query profile_os_definitions
- Poll SNMP OIDs for version and serial fields
- Display actual firmware version instead of profile name

For ePMP devices, this will now query:
- CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0 for firmware
- CAMBIUM-PMP80211-MIB::cambiumEPMPMSN.0 for serial number

The Operating System field will now show actual version like
'Cambium 4.7.1' instead of 'Cambium epmp'.
This commit is contained in:
Graham McIntire 2026-01-17 18:25:52 -06:00
parent 5baad658a3
commit b83484acf8
No known key found for this signature in database
4 changed files with 67 additions and 11 deletions

View file

@ -26,6 +26,7 @@ defmodule Towerops.Snmp.Device do
field :manufacturer, :string
field :model, :string
field :firmware_version, :string
field :serial_number, :string
belongs_to :device, DeviceSchema
@ -46,6 +47,7 @@ defmodule Towerops.Snmp.Device do
manufacturer: String.t() | nil,
model: String.t() | nil,
firmware_version: String.t() | nil,
serial_number: String.t() | nil,
device_id: Ecto.UUID.t(),
device: NotLoaded.t() | Devices.t(),
interfaces: NotLoaded.t() | [Interface.t()],
@ -67,7 +69,8 @@ defmodule Towerops.Snmp.Device do
:sys_location,
:manufacturer,
:model,
:firmware_version
:firmware_version,
:serial_number
])
|> validate_required([:device_id])
|> unique_constraint(:device_id)

View file

@ -253,13 +253,13 @@ defmodule Towerops.Snmp.Discovery do
@spec build_device_info(Client.connection_opts(), system_info(), profile()) ::
{:ok, device_info()}
defp build_device_info(_client_opts, system_info, {:dynamic, profile}) do
defp build_device_info(client_opts, system_info, {:dynamic, profile}) do
# Use dynamic profile to identify device
identified_info = Dynamic.identify_device(profile, system_info)
identified_info = Dynamic.identify_device(profile, client_opts, system_info)
{:ok, identified_info}
rescue
_error ->
Logger.error("Failed to identify device with dynamic profile")
error ->
Logger.error("Failed to identify device with dynamic profile: #{inspect(error)}")
{:ok, system_info}
end

View file

@ -17,17 +17,24 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
@doc """
Identifies device using the database profile information.
Returns manufacturer, model, and firmware info.
Polls SNMP OIDs from OS definitions to extract version and serial number.
"""
@spec identify_device(DeviceProfile.t(), Discovery.system_info()) :: Discovery.device_info()
def identify_device(profile, _system_info) do
@spec identify_device(DeviceProfile.t(), Client.connection_opts(), Discovery.system_info()) ::
Discovery.device_info()
def identify_device(profile, client_opts, _system_info) do
# Load profile with OS definitions
profile = DeviceProfiles.get_profile_with_associations(profile.os)
# Poll OS definitions for version, serial, etc.
os_data = poll_os_definitions(profile, client_opts)
# Use profile metadata to build device info
%{
manufacturer: extract_manufacturer(profile),
model: profile.text || profile.os,
# Use profile OS name as firmware_version for now
# TODO: Poll SNMP OIDs from profile_os_definitions to get actual version
firmware_version: profile.os,
serial_number: nil
firmware_version: os_data[:version] || profile.os,
serial_number: os_data[:serial]
}
end
@ -69,6 +76,43 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
# Private functions
defp poll_os_definitions(profile, client_opts) do
if profile && profile.os_definitions && length(profile.os_definitions) > 0 do
Logger.debug("Polling #{length(profile.os_definitions)} OS definitions for #{profile.os}")
# Poll each OS definition and build a map
profile.os_definitions
|> Enum.map(fn os_def ->
field = String.to_atom(os_def.field)
value = poll_os_definition_oid(client_opts, os_def)
{field, value}
end)
|> Enum.reject(fn {_field, value} -> is_nil(value) end)
|> Map.new()
else
%{}
end
end
defp poll_os_definition_oid(client_opts, os_def) do
# Use the OID from the OS definition
oid = os_def.oid
if oid do
case Client.get(client_opts, oid) do
{:ok, value} when is_binary(value) ->
value
{:ok, value} ->
to_string(value)
{:error, reason} ->
Logger.debug("Failed to poll OS definition OID #{oid}: #{inspect(reason)}")
nil
end
end
end
defp extract_manufacturer(%DeviceProfile{group: group}) when is_binary(group) do
# Capitalize manufacturer from group (e.g., "cambium" -> "Cambium")
String.capitalize(group)

View file

@ -0,0 +1,9 @@
defmodule Towerops.Repo.Migrations.AddSerialNumberToSnmpDevices do
use Ecto.Migration
def change do
alter table(:snmp_devices) do
add :serial_number, :string
end
end
end