feat: add check creation from discovery and backfill tool (Phase 5)
Completes Phase 5 of the unified checks system by enabling automatic check creation during SNMP discovery and providing a backfill tool for existing devices. Discovery Integration: - create_checks_from_discovery/2: Creates checks for all discovered entities (sensors, interfaces, processors, storage) after SNMP discovery completes - Links each check to its source entity via source_id for data lookup - Returns detailed results with counts per type and error tracking - Logs creation results with total counts and any failures Helper Functions: - create_sensor_check/2: Creates snmp_sensor checks with config - create_interface_check/2: Creates snmp_interface checks - create_processor_check/2: Creates snmp_processor checks - create_storage_check/2: Creates snmp_storage checks All checks configured with: - 60-second poll intervals (default) - Auto-discovery source type - Enabled by default - Type-specific configuration in JSONB config field Backfill Tool: - mix backfill.checks: Creates checks for existing SNMP-enabled devices - Supports --dry-run mode for preview - Shows counts per device (sensors, interfaces, processors, storage) - Reports success/failure counts and detailed errors - Safely handles devices without discovery data - Reduced nesting depth using cond and helper functions for credo compliance This enables the unified monitoring system to work with both new discoveries and existing devices, ensuring all SNMP data can be monitored through the unified checks interface. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d7741cdc0e
commit
75d64d8772
4 changed files with 304 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
103
lib/mix/tasks/backfill_checks.ex
Normal file
103
lib/mix/tasks/backfill_checks.ex
Normal file
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue