diff --git a/lib/towerops_web/channels/agent_channel.ex b/lib/towerops_web/channels/agent_channel.ex index 1e399a41..47ea5049 100644 --- a/lib/towerops_web/channels/agent_channel.ex +++ b/lib/towerops_web/channels/agent_channel.ex @@ -145,6 +145,38 @@ defmodule ToweropsWeb.AgentChannel do {:noreply, socket} end + # Poll a device immediately after discovery completes. + # Uses send(self(), ...) so discovery DB writes commit first. + def handle_info({:poll_after_discovery, device_id}, socket) do + case Devices.get_device_with_details(device_id) do + nil -> + Logger.error("Device not found for post-discovery poll", device_id: device_id) + + device -> + jobs = [build_polling_job(device)] + + jobs = + if device.monitoring_enabled do + jobs ++ [build_ping_job(device)] + else + jobs + end + + job_list = %AgentJobList{jobs: jobs} + binary = AgentJobList.encode(job_list) + + Logger.info("Sending immediate poll after discovery", + device_id: device_id, + device_name: device.name, + job_count: length(jobs) + ) + + push(socket, "jobs", %{binary: Base.encode64(binary)}) + end + + {:noreply, socket} + end + # Periodic heartbeat check — close channel if agent hasn't sent a heartbeat recently def handle_info(:check_heartbeat, socket) do seconds_since = DateTime.diff(DateTime.utc_now(), socket.assigns.last_heartbeat_at, :second) @@ -1249,6 +1281,9 @@ defmodule ToweropsWeb.AgentChannel do {:discovery_completed, device.id} ) + # Poll the device immediately so it doesn't sit idle until the next cycle + send(self(), {:poll_after_discovery, device.id}) + :ok {:error, reason} -> diff --git a/test/towerops_web/channels/agent_channel_test.exs b/test/towerops_web/channels/agent_channel_test.exs new file mode 100644 index 00000000..cc6f8504 --- /dev/null +++ b/test/towerops_web/channels/agent_channel_test.exs @@ -0,0 +1,102 @@ +defmodule ToweropsWeb.AgentChannelTest do + use Towerops.DataCase, async: false + + import Phoenix.ChannelTest + + alias Towerops.AccountsFixtures + alias Towerops.Agent.AgentJobList + alias Towerops.Agent.SnmpResult + alias Towerops.AgentsFixtures + alias Towerops.DevicesFixtures + alias Towerops.OrganizationsFixtures + alias ToweropsWeb.AgentSocket + + @endpoint ToweropsWeb.Endpoint + + setup do + # Create organization, site, device, and agent token + user = AccountsFixtures.user_fixture() + organization = OrganizationsFixtures.organization_fixture(user.id) + + device = + DevicesFixtures.device_fixture(%{ + organization_id: organization.id, + snmp_version: "2c", + snmp_community: "public" + }) + + {:ok, agent_token, token_string} = + AgentsFixtures.agent_token_fixture(organization.id) + + # Assign device to agent + {:ok, _assignment} = + AgentsFixtures.agent_assignment_fixture(agent_token.id, device.id) + + # Connect socket and join channel + {:ok, socket} = connect(AgentSocket, %{}) + + {:ok, _, socket} = + subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string}) + + # Drain the initial :send_jobs message pushed on join + assert_push "jobs", _initial_jobs + + %{ + socket: socket, + device: device, + agent_token: agent_token, + organization: organization + } + end + + describe "poll after discovery" do + test "pushes poll jobs after successful discovery result", %{ + socket: socket, + device: device + } do + # Build a minimal discovery result with system info OIDs + # that AgentDiscovery.process_agent_discovery can handle + discovery_result = %SnmpResult{ + device_id: device.id, + job_type: :DISCOVER, + job_id: "discover:#{device.id}", + timestamp: DateTime.to_unix(DateTime.utc_now()), + oid_values: %{ + # sysDescr + "1.3.6.1.2.1.1.1.0" => "Test Device Description", + # sysObjectID + "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1", + # sysUpTime + "1.3.6.1.2.1.1.3.0" => "123456", + # sysContact + "1.3.6.1.2.1.1.4.0" => "admin@test.com", + # sysName + "1.3.6.1.2.1.1.5.0" => "test-device", + # sysLocation + "1.3.6.1.2.1.1.6.0" => "Test Location" + } + } + + binary = SnmpResult.encode(discovery_result) + payload = %{"binary" => Base.encode64(binary)} + + # Send the discovery result to the channel + push(socket, "result", payload) + + # Should receive a "jobs" push with poll job(s) for the discovered device + assert_push "jobs", %{binary: jobs_binary}, 5_000 + + # Decode the job list and verify it contains a POLL job for our device + {:ok, decoded_binary} = Base.decode64(jobs_binary) + job_list = AgentJobList.decode(decoded_binary) + + poll_job = + Enum.find(job_list.jobs, fn job -> + job.job_type == :POLL and job.device_id == device.id + end) + + assert poll_job != nil, "Expected a POLL job for device #{device.id}" + assert poll_job.job_id == "poll:#{device.id}" + end + end +end