From b83484acf83e85e97f044fa9eac55bc664aedc8e Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 17 Jan 2026 18:25:52 -0600 Subject: [PATCH] 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'. --- lib/towerops/snmp/device.ex | 5 +- lib/towerops/snmp/discovery.ex | 8 +-- lib/towerops/snmp/profiles/dynamic.ex | 56 +++++++++++++++++-- ...2432_add_serial_number_to_snmp_devices.exs | 9 +++ 4 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 priv/repo/migrations/20260118002432_add_serial_number_to_snmp_devices.exs diff --git a/lib/towerops/snmp/device.ex b/lib/towerops/snmp/device.ex index 0c2106b3..a49344a6 100644 --- a/lib/towerops/snmp/device.ex +++ b/lib/towerops/snmp/device.ex @@ -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) diff --git a/lib/towerops/snmp/discovery.ex b/lib/towerops/snmp/discovery.ex index 51e5d62a..910827ba 100644 --- a/lib/towerops/snmp/discovery.ex +++ b/lib/towerops/snmp/discovery.ex @@ -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 diff --git a/lib/towerops/snmp/profiles/dynamic.ex b/lib/towerops/snmp/profiles/dynamic.ex index 0cfbaa71..ed8e20d7 100644 --- a/lib/towerops/snmp/profiles/dynamic.ex +++ b/lib/towerops/snmp/profiles/dynamic.ex @@ -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) diff --git a/priv/repo/migrations/20260118002432_add_serial_number_to_snmp_devices.exs b/priv/repo/migrations/20260118002432_add_serial_number_to_snmp_devices.exs new file mode 100644 index 00000000..a0e9b270 --- /dev/null +++ b/priv/repo/migrations/20260118002432_add_serial_number_to_snmp_devices.exs @@ -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