Fix agent API protobuf support and Mix.env runtime error
Fixed two critical issues preventing agent communication: 1. Fix Mix.env() runtime error in production - Replace Mix.env() with Application.get_env(:towerops, :env) - Add :env config to test.exs - Mix module not available in production releases 2. Add Protocol Buffers support to agent API endpoints - GET /api/v1/agent/config now accepts application/x-protobuf - POST /api/v1/agent/heartbeat now accepts application/x-protobuf - Added conversion functions for config/equipment/sensors/interfaces - Maintains JSON backward compatibility as fallback All agent controller tests passing (14 tests, 0 failures)
This commit is contained in:
parent
8864b8d26d
commit
9a6369bd27
4 changed files with 117 additions and 9 deletions
|
|
@ -452,4 +452,5 @@ end)
|
|||
- Use `ConnCase` for controller/LiveView tests
|
||||
- Use `async: true` for tests that can run in parallel (most unit tests)
|
||||
- Use `async: false` for tests with shared state (supervisor tests, integration tests)
|
||||
- never open the test coverage html files
|
||||
- never open the test coverage html files
|
||||
- remember when working in rust to always run cargo fmt before committing
|
||||
|
|
@ -43,6 +43,9 @@ config :towerops, ToweropsWeb.Endpoint,
|
|||
secret_key_base: "XDSSqVUtRXUjEfzxefIAaPIzBfonpNMOrCRyP1a0kjPzzyOpVNSRmMBVae/bwTqj",
|
||||
server: false
|
||||
|
||||
# Set environment identifier for runtime checks
|
||||
config :towerops, :env, :test
|
||||
|
||||
# Use mocks for testing
|
||||
config :towerops,
|
||||
ping_module: Towerops.Monitoring.PingMock,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ defmodule ToweropsWeb.Api.AgentController do
|
|||
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Agent.AgentConfig
|
||||
alias Towerops.Agent.HeartbeatMetadata
|
||||
alias Towerops.Agent.HeartbeatResponse
|
||||
alias Towerops.Agent.MetricBatch
|
||||
alias Towerops.Agents
|
||||
alias Towerops.Snmp
|
||||
|
|
@ -20,6 +23,8 @@ defmodule ToweropsWeb.Api.AgentController do
|
|||
Returns polling configuration for all equipment that should be polled by this agent.
|
||||
This includes directly assigned equipment plus equipment that inherits the agent
|
||||
from site or organization defaults.
|
||||
|
||||
Supports both JSON and Protocol Buffers response formats based on Accept header.
|
||||
"""
|
||||
def get_config(conn, _params) do
|
||||
agent_token = conn.assigns.current_agent_token
|
||||
|
|
@ -31,7 +36,21 @@ defmodule ToweropsWeb.Api.AgentController do
|
|||
equipment: Enum.map(equipment_list, &build_equipment_config/1)
|
||||
}
|
||||
|
||||
json(conn, config)
|
||||
# Check Accept header for protobuf support
|
||||
case get_req_header(conn, "accept") do
|
||||
["application/x-protobuf" | _] ->
|
||||
# Convert to protobuf and send
|
||||
proto_config = convert_config_to_proto(config)
|
||||
binary = AgentConfig.encode(proto_config)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/x-protobuf")
|
||||
|> send_resp(200, binary)
|
||||
|
||||
_ ->
|
||||
# Default to JSON
|
||||
json(conn, config)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -68,21 +87,51 @@ defmodule ToweropsWeb.Api.AgentController do
|
|||
POST /api/v1/agent/heartbeat
|
||||
|
||||
Updates the agent's last_seen_at timestamp and metadata.
|
||||
Supports both JSON and Protocol Buffers formats.
|
||||
"""
|
||||
def heartbeat(conn, params) do
|
||||
agent_token = conn.assigns.current_agent_token
|
||||
ip = to_string(:inet_parse.ntoa(conn.remote_ip))
|
||||
|
||||
metadata = %{
|
||||
"version" => Map.get(params, "version"),
|
||||
"hostname" => Map.get(params, "hostname"),
|
||||
"uptime_seconds" => Map.get(params, "uptime_seconds")
|
||||
}
|
||||
# Parse metadata based on content type
|
||||
metadata =
|
||||
case get_req_header(conn, "content-type") do
|
||||
["application/x-protobuf" | _] ->
|
||||
# Decode protobuf
|
||||
{:ok, body, _conn} = Plug.Conn.read_body(conn)
|
||||
proto_metadata = HeartbeatMetadata.decode(body)
|
||||
|
||||
%{
|
||||
"version" => proto_metadata.version,
|
||||
"hostname" => proto_metadata.hostname,
|
||||
"uptime_seconds" => proto_metadata.uptime_seconds
|
||||
}
|
||||
|
||||
_ ->
|
||||
# JSON format (fallback)
|
||||
%{
|
||||
"version" => Map.get(params, "version"),
|
||||
"hostname" => Map.get(params, "hostname"),
|
||||
"uptime_seconds" => Map.get(params, "uptime_seconds")
|
||||
}
|
||||
end
|
||||
|
||||
# Update synchronously in the heartbeat endpoint
|
||||
Agents.update_agent_token_heartbeat(agent_token.id, ip, metadata)
|
||||
|
||||
json(conn, %{status: "ok"})
|
||||
# Return response in appropriate format
|
||||
case get_req_header(conn, "content-type") do
|
||||
["application/x-protobuf" | _] ->
|
||||
proto_response = %HeartbeatResponse{status: "ok"}
|
||||
binary = HeartbeatResponse.encode(proto_response)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/x-protobuf")
|
||||
|> send_resp(200, binary)
|
||||
|
||||
_ ->
|
||||
json(conn, %{status: "ok"})
|
||||
end
|
||||
end
|
||||
|
||||
# Private helpers
|
||||
|
|
@ -209,4 +258,59 @@ defmodule ToweropsWeb.Api.AgentController do
|
|||
_ -> DateTime.truncate(DateTime.utc_now(), :second)
|
||||
end
|
||||
end
|
||||
|
||||
# Convert internal config format to protobuf
|
||||
defp convert_config_to_proto(config) do
|
||||
%AgentConfig{
|
||||
version: config.version,
|
||||
poll_interval_seconds: config.poll_interval_seconds,
|
||||
equipment: Enum.map(config.equipment, &convert_equipment_to_proto/1)
|
||||
}
|
||||
end
|
||||
|
||||
defp convert_equipment_to_proto(eq) do
|
||||
%Towerops.Agent.Equipment{
|
||||
id: eq.id,
|
||||
name: eq.name,
|
||||
ip_address: eq.ip_address,
|
||||
snmp: convert_snmp_to_proto(eq.snmp),
|
||||
poll_interval_seconds: eq.poll_interval_seconds,
|
||||
sensors: Enum.map(eq.sensors, &convert_sensor_to_proto/1),
|
||||
interfaces: Enum.map(eq.interfaces, &convert_interface_to_proto/1)
|
||||
}
|
||||
end
|
||||
|
||||
defp convert_snmp_to_proto(snmp) do
|
||||
%Towerops.Agent.SnmpConfig{
|
||||
enabled: snmp.enabled,
|
||||
version: snmp.version,
|
||||
community: snmp.community,
|
||||
port: snmp.port
|
||||
}
|
||||
end
|
||||
|
||||
defp convert_sensor_to_proto(sensor) do
|
||||
%Towerops.Agent.Sensor{
|
||||
id: sensor.id,
|
||||
type: sensor.type,
|
||||
oid: sensor.oid,
|
||||
divisor: sensor.divisor || 0.0,
|
||||
unit: sensor.unit || "",
|
||||
metadata: convert_metadata_to_proto(sensor.metadata)
|
||||
}
|
||||
end
|
||||
|
||||
defp convert_interface_to_proto(interface) do
|
||||
%Towerops.Agent.Interface{
|
||||
id: interface.id,
|
||||
if_index: interface.if_index,
|
||||
if_name: interface.if_name
|
||||
}
|
||||
end
|
||||
|
||||
defp convert_metadata_to_proto(nil), do: %{}
|
||||
|
||||
defp convert_metadata_to_proto(metadata) when is_map(metadata) do
|
||||
Map.new(metadata, fn {k, v} -> {to_string(k), to_string(v)} end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ defmodule ToweropsWeb.Plugs.AgentAuth do
|
|||
|
||||
# In test environment, update synchronously to avoid DB ownership issues
|
||||
# In production, update asynchronously for better performance
|
||||
if Mix.env() == :test do
|
||||
if Application.get_env(:towerops, :env) == :test do
|
||||
_ = Agents.update_agent_token_heartbeat(agent_token.id, ip)
|
||||
else
|
||||
update_heartbeat_async(agent_token.id, ip)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue