feat: add Phoenix support for agent-side ping monitoring
Enables Phoenix to send PING jobs to agents and receive monitoring check results, fixing the "unknown" status for devices assigned to local (non-cloud) agents. Changes: - Rename JobType::MONITOR to JobType::PING in protobuf for clarity - Add validate_monitoring_check_message() to Validator module - Add "monitoring_check" channel handler to receive ping results - Implement store_monitoring_check() to save results to database - Add build_ping_job() to generate PING jobs for agents - Update build_jobs_for_device() to include ping jobs when monitoring_enabled=true Phoenix now sends PING jobs alongside SNMP/MikroTik jobs, and agents send back MonitoringCheck results that are stored in the monitoring_checks table with proper agent_token_id tracking. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb8ffde119
commit
48dc4b852e
3 changed files with 118 additions and 1 deletions
|
|
@ -167,6 +167,19 @@ defmodule Towerops.Agent.Validator do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validate MonitoringCheck message from binary protobuf.
|
||||
|
||||
Decodes the binary and validates device_id, status enum, response_time range, and timestamp.
|
||||
"""
|
||||
@spec validate_monitoring_check_message(binary()) :: validation_result(MonitoringCheck.t())
|
||||
def validate_monitoring_check_message(binary) when is_binary(binary) do
|
||||
with {:ok, check} <- safe_decode(MonitoringCheck, binary),
|
||||
:ok <- validate_monitoring_check(check) do
|
||||
{:ok, check}
|
||||
end
|
||||
end
|
||||
|
||||
## Private Validation Functions
|
||||
|
||||
# Safe decode with error handling
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
alias Towerops.Devices
|
||||
alias Towerops.Devices.BackupRequests
|
||||
alias Towerops.Devices.MikrotikBackups
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Snmp
|
||||
alias Towerops.Snmp.AgentDiscovery
|
||||
alias Towerops.Snmp.Discovery
|
||||
|
|
@ -389,6 +390,43 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
end
|
||||
end
|
||||
|
||||
@spec handle_in(String.t(), %{required(String.t()) => base64_string()}, socket()) ::
|
||||
{:noreply, socket()}
|
||||
def handle_in("monitoring_check", %{"binary" => binary_b64}, socket) when is_binary(binary_b64) do
|
||||
with {:ok, binary} <- safe_base64_decode(binary_b64),
|
||||
{:ok, check} <- Validator.validate_monitoring_check_message(binary) do
|
||||
maybe_debug_log(socket, "Received monitoring check from agent",
|
||||
device_id: check.device_id,
|
||||
status: check.status,
|
||||
response_time_ms: check.response_time_ms,
|
||||
binary_size: byte_size(binary_b64)
|
||||
)
|
||||
|
||||
# Store monitoring check result in database
|
||||
_ = store_monitoring_check(check, socket)
|
||||
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:error, {type, message}} ->
|
||||
Logger.error("Invalid monitoring check from agent: #{type} - #{message}",
|
||||
agent_token_id: socket.assigns.agent_token_id,
|
||||
error_type: type,
|
||||
error_message: message,
|
||||
binary_size: byte_size(binary_b64)
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, :base64_decode_failed} ->
|
||||
Logger.error("Failed to decode monitoring check (invalid base64)",
|
||||
agent_token_id: socket.assigns.agent_token_id,
|
||||
binary_size: byte_size(binary_b64)
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
# Private helpers
|
||||
|
||||
@spec handle_validated_mikrotik_result(MikrotikResult.t(), socket()) :: {:noreply, socket()}
|
||||
|
|
@ -436,8 +474,14 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
build_mikrotik_job(device)
|
||||
end
|
||||
|
||||
# Build ping job if monitoring is enabled
|
||||
ping_job =
|
||||
if device.monitoring_enabled do
|
||||
build_ping_job(device)
|
||||
end
|
||||
|
||||
# Return list of jobs (filter out nil)
|
||||
Enum.reject([snmp_job, mikrotik_job], &is_nil/1)
|
||||
Enum.reject([snmp_job, mikrotik_job, ping_job], &is_nil/1)
|
||||
end
|
||||
|
||||
defp needs_discovery?(device) do
|
||||
|
|
@ -589,6 +633,22 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
}
|
||||
end
|
||||
|
||||
defp build_ping_job(device) do
|
||||
%AgentJob{
|
||||
job_id: "ping:#{device.id}",
|
||||
job_type: :PING,
|
||||
device_id: device.id,
|
||||
# Ping only needs IP address - use minimal SnmpDevice message
|
||||
snmp_device: %SnmpDevice{
|
||||
ip: to_string(device.ip_address),
|
||||
port: 0,
|
||||
version: "",
|
||||
community: ""
|
||||
},
|
||||
queries: []
|
||||
}
|
||||
end
|
||||
|
||||
defp build_live_polling_job(device, sensor_oids, reply_topic) do
|
||||
snmp_credentials = resolve_snmp_credentials(device)
|
||||
|
||||
|
|
@ -846,6 +906,49 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
end
|
||||
end
|
||||
|
||||
defp store_monitoring_check(check, socket) do
|
||||
organization_id = socket.assigns.organization_id
|
||||
agent_token_id = socket.assigns.agent_token_id
|
||||
|
||||
with {:ok, device} <- fetch_device(check.device_id),
|
||||
:ok <- verify_device_organization(device, organization_id) do
|
||||
# Convert protobuf timestamp to DateTime
|
||||
checked_at = DateTime.from_unix!(check.timestamp, :second)
|
||||
|
||||
# Convert status string to atom
|
||||
status = String.to_existing_atom(check.status)
|
||||
|
||||
attrs = %{
|
||||
device_id: check.device_id,
|
||||
agent_token_id: agent_token_id,
|
||||
status: status,
|
||||
response_time_ms: check.response_time_ms,
|
||||
checked_at: checked_at
|
||||
}
|
||||
|
||||
case Monitoring.create_check(attrs) do
|
||||
{:ok, _check} ->
|
||||
:ok
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.error("Failed to store monitoring check",
|
||||
device_id: check.device_id,
|
||||
errors: inspect(changeset.errors)
|
||||
)
|
||||
|
||||
{:error, :storage_failed}
|
||||
end
|
||||
else
|
||||
{:error, :device_not_found} ->
|
||||
Logger.error("Device not found for monitoring check: #{check.device_id}")
|
||||
{:error, :device_not_found}
|
||||
|
||||
{:error, :wrong_organization} ->
|
||||
Logger.error("Device #{check.device_id} not in agent's organization")
|
||||
{:error, :wrong_organization}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_device(device_id) do
|
||||
case Devices.get_device_with_details(device_id) do
|
||||
nil -> {:error, :device_not_found}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ enum JobType {
|
|||
POLL = 1;
|
||||
MIKROTIK = 2;
|
||||
TEST_CREDENTIALS = 3;
|
||||
PING = 4;
|
||||
}
|
||||
|
||||
enum QueryType {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue