diff --git a/lib/towerops/device_profiles/importer.ex b/lib/towerops/device_profiles/importer.ex index 7f08b63f..01330cc8 100644 --- a/lib/towerops/device_profiles/importer.ex +++ b/lib/towerops/device_profiles/importer.ex @@ -333,7 +333,9 @@ defmodule Towerops.DeviceProfiles.Importer do defp calculate_divisor(nil), do: 1 defp calculate_divisor(precision) when is_integer(precision) and precision > 0 do - 10 |> :math.pow(precision) |> trunc() + # Precision in device profiles is the divisor value itself, not an exponent + # For example: precision: 10 means divide by 10, precision: 1024 means divide by 1024 + precision end defp calculate_divisor(_), do: 1 diff --git a/lib/towerops/snmp/profiles/dynamic.ex b/lib/towerops/snmp/profiles/dynamic.ex index 152824d7..4a0c2bdc 100644 --- a/lib/towerops/snmp/profiles/dynamic.ex +++ b/lib/towerops/snmp/profiles/dynamic.ex @@ -51,25 +51,37 @@ defmodule Towerops.Snmp.Profiles.Dynamic do @doc """ Discovers sensors dynamically using database profile definitions. - Walks SNMP OIDs defined in the profile's sensor_definitions. + Walks SNMP OIDs defined in the profile's sensor_definitions and processor_definitions. """ @spec discover_sensors(DeviceProfile.t(), Client.connection_opts()) :: {:ok, [Discovery.sensor_data()]} def discover_sensors(profile, client_opts) do - # Load profile with sensor definitions + # Load profile with sensor and processor definitions profile = DeviceProfiles.get_profile_with_associations(profile.os) - if profile && length(profile.sensor_definitions) > 0 do - Logger.debug("Discovering sensors for #{profile.os} using #{length(profile.sensor_definitions)} definitions") + sensor_count = if profile, do: length(profile.sensor_definitions), else: 0 + processor_count = if profile, do: length(profile.processor_definitions), else: 0 + if profile && (sensor_count > 0 || processor_count > 0) do + Logger.debug( + "Discovering sensors for #{profile.os} (#{sensor_count} sensor defs, #{processor_count} processor defs)" + ) + + # Discover regular sensors sensors = Enum.flat_map(profile.sensor_definitions, fn sensor_def -> discover_sensor_from_definition(client_opts, sensor_def) end) - {:ok, sensors} + # Discover processors as sensors + cpu_sensors = + Enum.flat_map(profile.processor_definitions, fn proc_def -> + discover_processor_as_sensor(client_opts, proc_def) + end) + + {:ok, sensors ++ cpu_sensors} else - Logger.debug("No sensor definitions found for profile #{profile.os}") + Logger.debug("No sensor or processor definitions found for profile #{profile.os}") {:ok, []} end end @@ -99,6 +111,35 @@ defmodule Towerops.Snmp.Profiles.Dynamic do defp extract_manufacturer(_), do: "Unknown" + defp discover_processor_as_sensor(client_opts, proc_def) do + # Try numeric OID first, fall back to named OID + oid = proc_def.num_oid || proc_def.oid + + if oid do + # Remove template variables from OID + walk_oid = clean_oid_template(oid) + + case Client.walk(client_opts, walk_oid) do + {:ok, results} when is_map(results) and map_size(results) > 0 -> + # Convert walk results (map) to CPU sensor data + Enum.map(results, fn {result_oid, value} -> + build_processor_sensor_data(proc_def, %{oid: result_oid, value: value}) + end) + + {:ok, _empty} -> + Logger.debug("No data found for processor OID: #{walk_oid}") + [] + + {:error, reason} -> + Logger.warning("Failed to walk processor OID #{walk_oid}: #{inspect(reason)}") + [] + end + else + Logger.warning("Processor definition missing OID: #{inspect(proc_def)}") + [] + end + end + defp discover_sensor_from_definition(client_opts, sensor_def) do # Try numeric OID first, fall back to named OID oid = sensor_def.num_oid || sensor_def.oid @@ -109,10 +150,10 @@ defmodule Towerops.Snmp.Profiles.Dynamic do walk_oid = clean_oid_template(oid) case Client.walk(client_opts, walk_oid) do - {:ok, results} when is_list(results) and results != [] -> - # Convert walk results to sensor data - Enum.map(results, fn result -> - build_sensor_data(sensor_def, result) + {:ok, results} when is_map(results) and map_size(results) > 0 -> + # Convert walk results (map) to sensor data + Enum.map(results, fn {result_oid, value} -> + build_sensor_data(sensor_def, %{oid: result_oid, value: value}) end) {:ok, _empty} -> @@ -141,7 +182,11 @@ defmodule Towerops.Snmp.Profiles.Dynamic do defp build_sensor_data(sensor_def, %{oid: oid, value: value}) do # Build sensor index from OID (extract last component) - sensor_index = extract_index_from_oid(oid, sensor_def.index) + oid_index = extract_index_from_oid(oid, sensor_def.index) + + # Make index unique by combining sensor class with OID index + # This prevents conflicts when multiple sensors have the same OID index (e.g., ".0") + sensor_index = build_unique_sensor_index(sensor_def.sensor_class, sensor_def.descr, oid_index) # Apply divisor to value if it's numeric last_value = @@ -168,15 +213,54 @@ defmodule Towerops.Snmp.Profiles.Dynamic do } end + defp build_processor_sensor_data(proc_def, %{oid: oid, value: value}) do + # Build sensor index from OID (extract last component) + sensor_index = extract_index_from_oid(oid, proc_def.index) + + # Apply precision (divisor) to value if it's numeric + last_value = + cond do + is_number(value) && proc_def.precision && proc_def.precision > 1 -> + value / proc_def.precision + + is_number(value) -> + value * 1.0 + + true -> + nil + end + + # Determine status based on CPU load percentage + status = + cond do + is_nil(last_value) -> "unknown" + last_value < 70 -> "ok" + last_value < 90 -> "warning" + true -> "critical" + end + + %{ + sensor_type: "percent", + sensor_index: "cpu#{sensor_index}", + sensor_oid: oid, + sensor_descr: proc_def.descr || "CPU #{sensor_index}", + sensor_unit: "%", + sensor_divisor: proc_def.precision || 1, + last_value: last_value, + status: status + } + end + defp extract_index_from_oid(oid, configured_index) when is_binary(configured_index) do - # If index is pre-configured, use it - if configured_index == "" do + # If index contains template variable or is empty, extract from OID + if configured_index == "" or String.contains?(configured_index, "{{") do # Extract last component of OID as index oid |> String.split(".") |> List.last() |> to_string() else + # Use pre-configured static index configured_index end end @@ -189,6 +273,23 @@ defmodule Towerops.Snmp.Profiles.Dynamic do |> to_string() end + defp build_unique_sensor_index(sensor_class, descr, oid_index) do + # For scalar sensors (index 0), use a descriptive name from the description + # For table sensors, use the OID index + if oid_index == "0" && descr do + # Convert description to a safe index string + # E.g., "DFS Detected Count" -> "dfs_detected_count" + # E.g., "GPS Status" -> "gps_status" + descr + |> String.downcase() + |> String.replace(~r/[^a-z0-9]+/, "_") + |> String.trim("_") + else + # For table entries, prepend sensor class to OID index + "#{sensor_class}_#{oid_index}" + end + end + defp determine_unit("temperature"), do: "°C" defp determine_unit("voltage"), do: "V" defp determine_unit("current"), do: "A"