From 2324b75e4779f496e915617ecbe3785af1544bf1 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 18 Jan 2026 13:35:33 -0600 Subject: [PATCH] fix device identification --- lib/towerops/device_profiles/importer.ex | 64 +++++++++++++++---- lib/towerops/snmp/client.ex | 9 +++ lib/towerops/snmp/profiles/dynamic.ex | 21 ++++-- .../components/core_components.ex | 25 ++++++++ lib/towerops_web/components/layouts.ex | 2 +- .../live/agent_live/index.html.heex | 2 +- .../live/agent_live/show.html.heex | 11 ++-- 7 files changed, 113 insertions(+), 21 deletions(-) diff --git a/lib/towerops/device_profiles/importer.ex b/lib/towerops/device_profiles/importer.ex index 2a6ad96f..a07bcf82 100644 --- a/lib/towerops/device_profiles/importer.ex +++ b/lib/towerops/device_profiles/importer.ex @@ -164,22 +164,50 @@ defmodule Towerops.DeviceProfiles.Importer do defp create_sys_object_id_rules(profile_id, oids) when is_list(oids) do Enum.each(oids, fn oid -> - DeviceProfiles.create_detection_rule(%{ - device_profile_id: profile_id, - rule_type: "sysObjectID", - pattern: oid - }) + # Skip overly broad patterns that would match any enterprise OID + if is_overly_broad_oid?(oid) do + Logger.warning("Skipping overly broad sysObjectID pattern: #{oid}") + else + DeviceProfiles.create_detection_rule(%{ + device_profile_id: profile_id, + rule_type: "sysObjectID", + pattern: oid + }) + end end) {:ok, :created} end defp create_sys_object_id_rules(profile_id, oid) when is_binary(oid) do - DeviceProfiles.create_detection_rule(%{ - device_profile_id: profile_id, - rule_type: "sysObjectID", - pattern: oid - }) + # Skip overly broad patterns that would match any enterprise OID + if is_overly_broad_oid?(oid) do + Logger.warning("Skipping overly broad sysObjectID pattern: #{oid}") + else + DeviceProfiles.create_detection_rule(%{ + device_profile_id: profile_id, + rule_type: "sysObjectID", + pattern: oid + }) + end + end + + # Check if an OID pattern is too broad and would match too many devices + # Patterns like ".1.3.6.1.4.1" would match ANY enterprise OID + defp is_overly_broad_oid?(oid) do + # Normalize to dotted format + normalized = if String.starts_with?(oid, "."), do: oid, else: "." <> oid + + # These are the only acceptable root OIDs that are specific enough + # .1.3.6.1.4.1 is the enterprise prefix - anything shorter is too broad + # Must have at least one more component after .1.3.6.1.4.1 + case normalized do + ".1.3.6.1.4.1" -> true + # Also reject other common roots that are too broad + ".1.3.6.1.4" -> true + ".1.3.6.1" -> true + _ -> false + end end defp create_sys_descr_regex_rules(profile_id, patterns) when is_list(patterns) do @@ -418,8 +446,22 @@ defmodule Towerops.DeviceProfiles.Importer do defp present?(_), do: true # Convert OID values, preserving all data types since the field is now JSONB - # Only convert empty strings to nil + # Handles: + # - Empty strings -> nil + # - Arrays (data corruption) -> extract first element if it's a string + # - Everything else -> pass through defp nilify_blank(nil), do: nil defp nilify_blank(""), do: nil + + defp nilify_blank([single_oid | _rest]) when is_binary(single_oid) do + Logger.warning("OID stored as array (data corruption), extracting: #{single_oid}") + single_oid + end + + defp nilify_blank([_non_string | _rest] = value) do + Logger.warning("OID array contains non-string values, skipping: #{inspect(value)}") + nil + end + defp nilify_blank(value), do: value end diff --git a/lib/towerops/snmp/client.ex b/lib/towerops/snmp/client.ex index 246e49a7..9266e6ca 100644 --- a/lib/towerops/snmp/client.ex +++ b/lib/towerops/snmp/client.ex @@ -50,6 +50,9 @@ defmodule Towerops.Snmp.Client do :no_such_instance -> Logger.debug("SNMP instance not found for #{target}: #{inspect(oid)}") + :no_such_name -> + Logger.debug("SNMP name not found for #{target}: #{inspect(oid)}") + :end_of_mib_view -> Logger.debug("End of MIB view reached for #{target}: #{inspect(oid)}") @@ -133,6 +136,9 @@ defmodule Towerops.Snmp.Client do :no_such_instance -> Logger.debug("SNMP WALK: instance not found for #{target} at #{inspect(start_oid)}") + :no_such_name -> + Logger.debug("SNMP WALK: name not found for #{target} at #{inspect(start_oid)}") + :end_of_mib_view -> Logger.debug("SNMP WALK: end of MIB view for #{target} at #{inspect(start_oid)}") @@ -185,6 +191,9 @@ defmodule Towerops.Snmp.Client do :no_such_instance -> Logger.debug("SNMP GET-BULK: instance not found for #{target} at #{inspect(start_oid)}") + :no_such_name -> + Logger.debug("SNMP GET-BULK: name not found for #{target} at #{inspect(start_oid)}") + :end_of_mib_view -> Logger.debug("SNMP GET-BULK: end of MIB view for #{target} at #{inspect(start_oid)}") diff --git a/lib/towerops/snmp/profiles/dynamic.ex b/lib/towerops/snmp/profiles/dynamic.ex index a488ece0..0f41705d 100644 --- a/lib/towerops/snmp/profiles/dynamic.ex +++ b/lib/towerops/snmp/profiles/dynamic.ex @@ -148,10 +148,23 @@ defmodule Towerops.Snmp.Profiles.Dynamic do oid = sensor_def.num_oid || sensor_def.oid if oid do - # Remove template variables like ".{{ $index }}" from OID - # For walks, we need the base OID without the index placeholder - walk_oid = clean_oid_template(oid) - walk_sensor_oid(client_opts, walk_oid, sensor_def) + # Handle case where OID is mistakenly stored as array (data corruption) + oid_string = + case oid do + [single_oid | _] when is_binary(single_oid) -> single_oid + oid when is_binary(oid) -> oid + _ -> nil + end + + if oid_string do + # Remove template variables like ".{{ $index }}" from OID + # For walks, we need the base OID without the index placeholder + walk_oid = clean_oid_template(oid_string) + walk_sensor_oid(client_opts, walk_oid, sensor_def) + else + Logger.warning("Sensor definition has invalid OID format: #{inspect(oid)}") + [] + end else Logger.warning("Sensor definition missing OID: #{inspect(sensor_def)}") [] diff --git a/lib/towerops_web/components/core_components.ex b/lib/towerops_web/components/core_components.ex index aba09cbb..bba3d64c 100644 --- a/lib/towerops_web/components/core_components.ex +++ b/lib/towerops_web/components/core_components.ex @@ -576,6 +576,31 @@ defmodule ToweropsWeb.CoreComponents do end end + @doc """ + Renders a relative timestamp with full timestamp on hover. + + ## Examples + + <.timestamp datetime={@device.inserted_at} timezone={@timezone} /> + + """ + attr :datetime, :any, required: true, doc: "DateTime to display" + attr :timezone, :string, default: "UTC", doc: "User's timezone for full timestamp" + attr :class, :string, default: "", doc: "Additional CSS classes" + + def timestamp(assigns) do + assigns = + assigns + |> assign(:relative_time, ToweropsWeb.TimeHelpers.format_time_ago(assigns.datetime)) + |> assign(:full_time, ToweropsWeb.TimeHelpers.format_datetime(assigns.datetime, assigns.timezone)) + + ~H""" + + {@relative_time} + + """ + end + @doc """ Translates the errors for a field from a keyword list of errors. """ diff --git a/lib/towerops_web/components/layouts.ex b/lib/towerops_web/components/layouts.ex index 2c601cd5..f70f212c 100644 --- a/lib/towerops_web/components/layouts.ex +++ b/lib/towerops_web/components/layouts.ex @@ -459,7 +459,7 @@ defmodule ToweropsWeb.Layouts do

Copyright © {Date.utc_today().year} Towerops

- Last deploy {@time_ago} · {@formatted_time} + Last deploy {@time_ago} · {@formatted_time}

made in diff --git a/lib/towerops_web/live/agent_live/index.html.heex b/lib/towerops_web/live/agent_live/index.html.heex index a1d4d4ac..f252e0f1 100644 --- a/lib/towerops_web/live/agent_live/index.html.heex +++ b/lib/towerops_web/live/agent_live/index.html.heex @@ -100,7 +100,7 @@ <:col :let={agent} label="Last Seen">
- {format_last_seen(agent.last_seen_at)} + <.timestamp datetime={agent.last_seen_at} timezone={@timezone} />
<%= if agent.last_seen_at do %>
diff --git a/lib/towerops_web/live/agent_live/show.html.heex b/lib/towerops_web/live/agent_live/show.html.heex index e7b1398f..576c16aa 100644 --- a/lib/towerops_web/live/agent_live/show.html.heex +++ b/lib/towerops_web/live/agent_live/show.html.heex @@ -65,7 +65,10 @@

Last Seen

- {format_last_seen(@agent_token.last_seen_at)} + <.timestamp + datetime={@agent_token.last_seen_at} + timezone={@timezone} + />

<%= if @agent_token.last_seen_at do %>

@@ -154,7 +157,7 @@ {format_datetime(@agent_token.inserted_at, @timezone)}

- {format_last_seen(@agent_token.inserted_at)} (created) + <.timestamp datetime={@agent_token.inserted_at} timezone={@timezone} /> (created)
@@ -164,7 +167,7 @@ {format_datetime(@agent_token.updated_at, @timezone)}
- {format_last_seen(@agent_token.updated_at)} + <.timestamp datetime={@agent_token.updated_at} timezone={@timezone} />
@@ -177,7 +180,7 @@ <%= if @agent_token.last_seen_at do %>
- {format_last_seen(@agent_token.last_seen_at)} + <.timestamp datetime={@agent_token.last_seen_at} timezone={@timezone} />
<% end %>