fix device identification

This commit is contained in:
Graham McIntire 2026-01-18 13:35:33 -06:00
parent 8e1703cf28
commit 2324b75e47
No known key found for this signature in database
7 changed files with 113 additions and 21 deletions

View file

@ -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

View file

@ -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)}")

View file

@ -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)}")
[]

View file

@ -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"""
<span class={@class} title={@full_time}>
{@relative_time}
</span>
"""
end
@doc """
Translates the errors for a field from a keyword list of errors.
"""

View file

@ -459,7 +459,7 @@ defmodule ToweropsWeb.Layouts do
<div class="flex flex-col items-center gap-2 text-sm text-zinc-600 dark:text-zinc-400">
<p>Copyright © {Date.utc_today().year} Towerops</p>
<p class="text-xs">
Last deploy {@time_ago} · {@formatted_time}
Last deploy <span title={@formatted_time}>{@time_ago}</span> · {@formatted_time}
</p>
<div class="flex items-center gap-1 text-xs opacity-60">
<span class="leading-none">made in</span>

View file

@ -100,7 +100,7 @@
<:col :let={agent} label="Last Seen">
<div class="text-sm text-zinc-900 dark:text-zinc-100">
{format_last_seen(agent.last_seen_at)}
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} />
</div>
<%= if agent.last_seen_at do %>
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">

View file

@ -65,7 +65,10 @@
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Last Seen</p>
<p class="mt-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{format_last_seen(@agent_token.last_seen_at)}
<.timestamp
datetime={@agent_token.last_seen_at}
timezone={@timezone}
/>
</p>
<%= if @agent_token.last_seen_at do %>
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
@ -154,7 +157,7 @@
{format_datetime(@agent_token.inserted_at, @timezone)}
</dd>
<dd class="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{format_last_seen(@agent_token.inserted_at)} (created)
<.timestamp datetime={@agent_token.inserted_at} timezone={@timezone} /> (created)
</dd>
</div>
@ -164,7 +167,7 @@
{format_datetime(@agent_token.updated_at, @timezone)}
</dd>
<dd class="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{format_last_seen(@agent_token.updated_at)}
<.timestamp datetime={@agent_token.updated_at} timezone={@timezone} />
</dd>
</div>
@ -177,7 +180,7 @@
</dd>
<%= if @agent_token.last_seen_at do %>
<dd class="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{format_last_seen(@agent_token.last_seen_at)}
<.timestamp datetime={@agent_token.last_seen_at} timezone={@timezone} />
</dd>
<% end %>
</div>