diff --git a/CHANGELOG.txt b/CHANGELOG.txt index eb938004..e04e6bce 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,17 @@ +2026-02-12 +feat: add check creation from SNMP discovery and backfill task (Phase 5) + - Implemented create_checks_from_discovery/2 in Discovery module to auto-create + checks for all discovered SNMP entities (sensors, interfaces, processors, storage) + - Discovery now creates check records enabling unified monitoring after completing + SNMP discovery, linking checks to source entities via source_id + - Added mix backfill.checks task to create checks for existing devices with + SNMP discovery data, includes dry-run mode and comprehensive error reporting + - Returns detailed results map with counts per entity type and any errors + - Private helper functions: create_sensor_check/2, create_interface_check/2, + create_processor_check/2, create_storage_check/2 + - All checks default to 60-second intervals, enabled state, auto_discovery source + - Files: lib/towerops/snmp/discovery.ex, lib/mix/tasks/backfill_checks.ex + 2026-02-12 docs: add Terraform provider documentation to public API docs - Added comprehensive Terraform provider section to /docs/api page with navigation link diff --git a/lib/mix/tasks/backfill_checks.ex b/lib/mix/tasks/backfill_checks.ex new file mode 100644 index 00000000..08c726b8 --- /dev/null +++ b/lib/mix/tasks/backfill_checks.ex @@ -0,0 +1,103 @@ +defmodule Mix.Tasks.Backfill.Checks do + @shortdoc "Backfill checks for existing SNMP-enabled devices" + + @moduledoc """ + Backfill checks for existing devices with SNMP discovery data. + + This task creates check records for all devices that have existing + snmp_device records with sensors, interfaces, processors, or storage. + + Usage: + mix backfill.checks + mix backfill.checks --dry-run + """ + + use Mix.Task + + import Ecto.Query + + alias Towerops.Devices + alias Towerops.Repo + alias Towerops.Snmp.Discovery + + require Logger + + @impl Mix.Task + def run(args) do + Mix.Task.run("app.start") + + dry_run = "--dry-run" in args + + if dry_run do + IO.puts("=== DRY RUN MODE - No changes will be made ===\n") + end + + devices = + Repo.all( + from d in Devices.Device, + where: d.snmp_enabled == true, + preload: [:organization, snmp_device: [:sensors, :interfaces, :processors, :storage]] + ) + + IO.puts("Found #{length(devices)} SNMP-enabled devices\n") + + Enum.each(devices, fn device -> + if device.snmp_device do + backfill_device_checks(device, dry_run) + else + IO.puts("Skipping #{device.name} - no SNMP discovery data") + end + end) + + IO.puts("\n=== Backfill complete ===") + end + + defp backfill_device_checks(device, dry_run) do + snmp_device = device.snmp_device + sensor_count = length(snmp_device.sensors) + interface_count = length(snmp_device.interfaces) + processor_count = length(snmp_device.processors) + storage_count = length(snmp_device.storage) + + total = sensor_count + interface_count + processor_count + storage_count + + cond do + total == 0 -> + IO.puts("Skipping #{device.name} - no discovered entities") + + dry_run -> + print_device_summary(device, sensor_count, interface_count, processor_count, storage_count) + IO.puts(" [DRY RUN] Would create #{total} checks") + + true -> + print_device_summary(device, sensor_count, interface_count, processor_count, storage_count) + create_and_report_checks(device, snmp_device) + end + end + + defp print_device_summary(device, sensors, interfaces, processors, storage) do + IO.puts("\n#{device.name} (#{device.ip_address}):") + IO.puts(" - #{sensors} sensors") + IO.puts(" - #{interfaces} interfaces") + IO.puts(" - #{processors} processors") + IO.puts(" - #{storage} storage") + end + + defp create_and_report_checks(device, snmp_device) do + result = Discovery.create_checks_from_discovery(device, snmp_device) + created = result.sensors + result.interfaces + result.processors + result.storage + + if result.errors == [] do + IO.puts(" ✓ Created #{created} checks") + else + IO.puts(" ✓ Created #{created} checks (#{length(result.errors)} errors)") + print_errors(result.errors) + end + end + + defp print_errors(errors) do + Enum.each(errors, fn error -> + IO.puts(" ⚠ #{error.type}: #{error.message}") + end) + end +end diff --git a/lib/towerops/snmp/discovery.ex b/lib/towerops/snmp/discovery.ex index ece5a8fd..fec61ec9 100644 --- a/lib/towerops/snmp/discovery.ex +++ b/lib/towerops/snmp/discovery.ex @@ -418,8 +418,194 @@ defmodule Towerops.Snmp.Discovery do {:ok, results} end + @doc """ + Creates checks for all discovered SNMP entities (sensors, interfaces, processors, storage). + + This function is called after SNMP discovery completes to create check records + that enable monitoring of the discovered entities. + + ## Parameters + - device: The Device schema struct + - snmp_device: The SnmpDevice with preloaded sensors, interfaces, processors, storage + + ## Returns + A map with counts of created checks and any errors: + ``` + %{ + sensors: 10, + interfaces: 5, + processors: 2, + storage: 3, + errors: [] + } + ``` + """ + def create_checks_from_discovery(device, snmp_device) do + alias Towerops.Monitoring + + # Ensure associations are loaded + snmp_device = Repo.preload(snmp_device, [:sensors, :interfaces, :processors, :storage]) + + result = %{ + sensors: 0, + interfaces: 0, + processors: 0, + storage: 0, + errors: [] + } + + # Create checks for sensors + result = + Enum.reduce(snmp_device.sensors, result, fn sensor, acc -> + case create_sensor_check(device, sensor) do + {:ok, _check} -> %{acc | sensors: acc.sensors + 1} + {:error, reason} -> %{acc | errors: [%{type: "sensor", id: sensor.id, message: inspect(reason)} | acc.errors]} + end + end) + + # Create checks for interfaces + result = + Enum.reduce(snmp_device.interfaces, result, fn interface, acc -> + case create_interface_check(device, interface) do + {:ok, _check} -> + %{acc | interfaces: acc.interfaces + 1} + + {:error, reason} -> + %{acc | errors: [%{type: "interface", id: interface.id, message: inspect(reason)} | acc.errors]} + end + end) + + # Create checks for processors + result = + Enum.reduce(snmp_device.processors, result, fn processor, acc -> + case create_processor_check(device, processor) do + {:ok, _check} -> + %{acc | processors: acc.processors + 1} + + {:error, reason} -> + %{acc | errors: [%{type: "processor", id: processor.id, message: inspect(reason)} | acc.errors]} + end + end) + + # Create checks for storage + result = + Enum.reduce(snmp_device.storage, result, fn storage, acc -> + case create_storage_check(device, storage) do + {:ok, _check} -> %{acc | storage: acc.storage + 1} + {:error, reason} -> %{acc | errors: [%{type: "storage", id: storage.id, message: inspect(reason)} | acc.errors]} + end + end) + + # Log results + log_check_creation_results(device.id, result) + + result + end + # Private functions + defp create_sensor_check(device, sensor) do + alias Towerops.Monitoring + + Monitoring.create_check(%{ + organization_id: device.organization_id, + device_id: device.id, + name: sensor.sensor_descr, + check_type: "snmp_sensor", + source_type: "auto_discovery", + source_id: sensor.id, + interval_seconds: 60, + enabled: true, + config: %{ + "sensor_type" => sensor.sensor_type, + "sensor_class" => sensor.sensor_class, + "sensor_oid" => sensor.sensor_oid, + "sensor_divisor" => sensor.sensor_divisor, + "sensor_unit" => sensor.sensor_unit + } + }) + end + + defp create_interface_check(device, interface) do + alias Towerops.Monitoring + + Monitoring.create_check(%{ + organization_id: device.organization_id, + device_id: device.id, + name: "Interface #{interface.if_descr}", + check_type: "snmp_interface", + source_type: "auto_discovery", + source_id: interface.id, + interval_seconds: 60, + enabled: true, + config: %{ + "if_index" => interface.if_index, + "if_descr" => interface.if_descr + } + }) + end + + defp create_processor_check(device, processor) do + alias Towerops.Monitoring + + Monitoring.create_check(%{ + organization_id: device.organization_id, + device_id: device.id, + name: "CPU #{processor.processor_index}", + check_type: "snmp_processor", + source_type: "auto_discovery", + source_id: processor.id, + interval_seconds: 60, + enabled: true, + config: %{ + "processor_index" => processor.processor_index, + "processor_descr" => processor.processor_descr + } + }) + end + + defp create_storage_check(device, storage) do + alias Towerops.Monitoring + + Monitoring.create_check(%{ + organization_id: device.organization_id, + device_id: device.id, + name: storage.description || storage.device_name || "Storage #{storage.storage_index}", + check_type: "snmp_storage", + source_type: "auto_discovery", + source_id: storage.id, + interval_seconds: 60, + enabled: true, + config: %{ + "storage_index" => storage.storage_index, + "storage_descr" => storage.description + } + }) + end + + defp log_check_creation_results(device_id, check_counts) do + total_checks = + check_counts.sensors + + check_counts.interfaces + + check_counts.processors + + check_counts.storage + + Logger.info( + "Created #{total_checks} checks for device #{device_id}: " <> + "#{check_counts.sensors} sensors, #{check_counts.interfaces} interfaces, " <> + "#{check_counts.processors} processors, #{check_counts.storage} storage", + device_id: device_id + ) + + if check_counts.errors != [] do + Logger.warning( + "Failed to create #{length(check_counts.errors)} checks for device #{device_id}", + device_id: device_id, + errors: check_counts.errors + ) + end + end + @spec build_client_opts(DeviceSchema.t()) :: Client.connection_opts() defp build_client_opts(device) do # Get SNMP config with hierarchical fallback (device -> site -> organization) @@ -1191,26 +1377,6 @@ defmodule Towerops.Snmp.Discovery do :ok end - defp log_check_creation_results(device_id, check_counts) do - total_checks = - check_counts.sensors + check_counts.interfaces + check_counts.processors + check_counts.storage - - Logger.info( - "Created #{total_checks} checks: " <> - "#{check_counts.sensors} sensors, #{check_counts.interfaces} interfaces, " <> - "#{check_counts.processors} processors, #{check_counts.storage} storage", - device_id: device_id - ) - - if check_counts.errors != [] do - Logger.warning( - "Failed to create #{length(check_counts.errors)} checks", - device_id: device_id, - errors: check_counts.errors - ) - end - end - defp log_discovery_event(device, discovered_device, is_rediscovery) do event_type = if is_rediscovery, do: "device_rediscovered", else: "device_discovered" diff --git a/priv/static/changelog.txt b/priv/static/changelog.txt index 978f7857..109ae421 100644 --- a/priv/static/changelog.txt +++ b/priv/static/changelog.txt @@ -9,6 +9,7 @@ Devices Tested & Working * Device checks tab showing all monitored items (sensors, interfaces, services) with status indicators * Manual service check creation for HTTP/HTTPS, TCP port, and DNS monitoring * Automatic check creation from SNMP discovery (sensors, interfaces, processors, storage) +* Backfill tool to create checks for existing devices with SNMP data * Unified time-series graphing across all check types * Enhanced monitoring infrastructure with improved reliability * Fixed SNMP monitoring credential resolution for all check types