fix siteless devices not inheriting org default agent

devices assigned directly to an org (no site) were excluded from
apply_agent_to_all_equipment due to inner join through site table.
also fix fallback agent resolution to check device.organization
when device.site is nil.
This commit is contained in:
Graham McIntire 2026-02-11 12:20:53 -06:00
parent bd24dfcbed
commit 92eb583a8b
No known key found for this signature in database
6 changed files with 41 additions and 33 deletions

View file

@ -710,11 +710,11 @@ defmodule Towerops.Agents do
Returns the agent_token_id if found, nil otherwise.
The device must be preloaded with site: [organization: :default_agent_token].
The device must be preloaded with `[:organization, site: [organization: :default_agent_token]]`.
## Examples
iex> device = Repo.preload(device, site: [organization: :default_agent_token])
iex> device = Repo.preload(device, [:organization, site: [organization: :default_agent_token]])
iex> get_effective_agent_token(device)
"agent-token-uuid"
@ -730,21 +730,27 @@ defmodule Towerops.Agents do
agent_token_id
_ ->
get_fallback_agent_token(device.site)
get_fallback_agent_token(device)
end
end
# Checks site, organization, and global default in sequence
defp get_fallback_agent_token(site) do
case site do
%{agent_token_id: agent_token_id} when not is_nil(agent_token_id) ->
agent_token_id
defp get_fallback_agent_token(device) do
cond do
# 2. Site-level agent
match?(%{agent_token_id: id} when not is_nil(id), device.site) ->
device.site.agent_token_id
%{organization: %{default_agent_token_id: agent_token_id}}
when not is_nil(agent_token_id) ->
agent_token_id
# 3. Organization default via site
match?(%{organization: %{default_agent_token_id: id}} when not is_nil(id), device.site) ->
device.site.organization.default_agent_token_id
_ ->
# 3b. Organization default directly (device has no site)
match?(%{default_agent_token_id: id} when not is_nil(id), device.organization) ->
device.organization.default_agent_token_id
# 4. Global default
true ->
Towerops.Settings.get_global_default_cloud_poller()
end
end
@ -777,21 +783,27 @@ defmodule Towerops.Agents do
{agent_token_id, :device}
_ ->
get_fallback_agent_token_with_source(device.site)
get_fallback_agent_token_with_source(device)
end
end
# Checks site, organization, and global default in sequence with source tracking
defp get_fallback_agent_token_with_source(site) do
case site do
%{agent_token_id: agent_token_id} when not is_nil(agent_token_id) ->
{agent_token_id, :site}
defp get_fallback_agent_token_with_source(device) do
cond do
# 2. Site-level agent
match?(%{agent_token_id: id} when not is_nil(id), device.site) ->
{device.site.agent_token_id, :site}
%{organization: %{default_agent_token_id: agent_token_id}}
when not is_nil(agent_token_id) ->
{agent_token_id, :organization}
# 3. Organization default via site
match?(%{organization: %{default_agent_token_id: id}} when not is_nil(id), device.site) ->
{device.site.organization.default_agent_token_id, :organization}
_ ->
# 3b. Organization default directly (device has no site)
match?(%{default_agent_token_id: id} when not is_nil(id), device.organization) ->
{device.organization.default_agent_token_id, :organization}
# 4. Global default
true ->
case Towerops.Settings.get_global_default_cloud_poller() do
agent_token_id when not is_nil(agent_token_id) ->
{agent_token_id, :global}

View file

@ -378,7 +378,7 @@ defmodule Towerops.Agents.Stats do
device =
Device
|> Repo.get!(device_id)
|> Repo.preload(site: [organization: :default_agent_token])
|> Repo.preload([:organization, site: [organization: :default_agent_token]])
{current_agent_id, source} = Towerops.Agents.get_effective_agent_token_with_source(device)
@ -453,7 +453,7 @@ defmodule Towerops.Agents.Stats do
device =
Device
|> Repo.get!(device_id)
|> Repo.preload(site: [organization: :default_agent_token])
|> Repo.preload([:organization, site: [organization: :default_agent_token]])
{current_agent_id, source} = Towerops.Agents.get_effective_agent_token_with_source(device)

View file

@ -241,7 +241,7 @@ defmodule Towerops.Devices do
nil
device ->
Repo.preload(device, [:site, snmp_device: [:interfaces, :sensors]])
Repo.preload(device, [:site, :organization, snmp_device: [:interfaces, :sensors]])
end
end
@ -653,7 +653,7 @@ defmodule Towerops.Devices do
defp validate_cloud_poller_requires_ssl(changeset, device) do
# Get effective agent to check if it's a cloud poller
device_with_site = Repo.preload(device, site: :organization)
device_with_site = Repo.preload(device, [:organization, site: :organization])
{effective_agent_id, _source} = Agents.get_effective_agent_token_with_source(device_with_site)
using_cloud_poller =

View file

@ -415,11 +415,9 @@ defmodule Towerops.Organizations do
organization = Repo.get!(Organization, organization_id)
if organization.default_agent_token_id do
# device IDs for this organization
# device IDs for this organization (includes devices with and without sites)
device_ids =
Repo.all(
from(e in Device, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
)
Repo.all(from(e in Device, where: e.organization_id == ^organization_id, select: e.id))
# Delete existing assignments
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
@ -442,9 +440,7 @@ defmodule Towerops.Organizations do
else
# No default agent set, delete all assignments
device_ids =
Repo.all(
from(e in Device, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
)
Repo.all(from(e in Device, where: e.organization_id == ^organization_id, select: e.id))
Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
end

View file

@ -101,7 +101,7 @@ defmodule ToweropsWeb.DeviceLive.Form do
case AccessControl.verify_device_access(id, organization.id) do
{:ok, device} ->
# Preload necessary associations for agent resolution
device = Repo.preload(device, site: [organization: :default_agent_token])
device = Repo.preload(device, [:organization, site: [organization: :default_agent_token]])
apply_edit_action(socket, device)
{:error, :not_found} ->

View file

@ -222,7 +222,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
defp load_equipment_data(socket, device_id) do
device = Devices.get_device!(device_id)
device_with_associations = Repo.preload(device, site: [organization: :default_agent_token])
device_with_associations = Repo.preload(device, [:organization, site: [organization: :default_agent_token]])
{agent_token_id, agent_source} = Agents.get_effective_agent_token_with_source(device_with_associations)
agent_info = load_agent_info(agent_token_id, agent_source)
recent_checks = Monitoring.list_devices_checks(device_id, 50)