From dca90db13710dabc60c0a8cde203c7b6105c8856 Mon Sep 17 00:00:00 2001
From: Graham McIntire
Date: Sat, 17 Jan 2026 12:05:08 -0600
Subject: [PATCH] allow override of agent and snmp community
---
lib/towerops/organizations.ex | 71 +++++++++++++++
lib/towerops/sites.ex | 63 +++++++++++++
lib/towerops_web/live/org/settings_live.ex | 14 +++
.../live/org/settings_live.html.heex | 39 ++++++++
lib/towerops_web/live/site_live/form.ex | 14 +++
.../live/site_live/form.html.heex | 90 ++++++++++++++-----
6 files changed, 268 insertions(+), 23 deletions(-)
diff --git a/lib/towerops/organizations.ex b/lib/towerops/organizations.ex
index aca55206..02fb7e60 100644
--- a/lib/towerops/organizations.ex
+++ b/lib/towerops/organizations.ex
@@ -5,6 +5,8 @@ defmodule Towerops.Organizations do
import Ecto.Query
+ alias Towerops.Agents.AgentAssignment
+ alias Towerops.Equipment.Equipment
alias Towerops.Organizations.Invitation
alias Towerops.Organizations.Membership
alias Towerops.Organizations.Organization
@@ -248,6 +250,75 @@ defmodule Towerops.Organizations do
Repo.delete(invitation)
end
+ ## Bulk Configuration Updates
+
+ @doc """
+ Applies organization's SNMP configuration to all equipment in the organization.
+
+ This updates all equipment records to use the organization's SNMP version and community string,
+ overwriting any existing equipment-level or site-level configurations.
+
+ Returns the number of equipment records updated.
+ """
+ def apply_snmp_config_to_all_equipment(organization_id) do
+ organization = get_organization!(organization_id)
+
+ Repo.update_all(from(e in Equipment, join: s in assoc(e, :site), where: s.organization_id == ^organization_id),
+ set: [
+ snmp_version: organization.snmp_version,
+ snmp_community: organization.snmp_community,
+ updated_at: DateTime.truncate(DateTime.utc_now(), :second)
+ ]
+ )
+ end
+
+ @doc """
+ Applies organization's default agent to all equipment in the organization.
+
+ This creates/updates agent assignments for all equipment to use the organization's
+ default agent, overwriting any existing equipment-level or site-level assignments.
+
+ Returns the number of equipment records assigned.
+ """
+ def apply_agent_to_all_equipment(organization_id) do
+ organization = Repo.get!(Organization, organization_id)
+
+ if organization.default_agent_token_id do
+ # Get all equipment IDs for this organization
+ equipment_ids =
+ Repo.all(
+ from(e in Equipment, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
+ )
+
+ # Delete existing assignments
+ Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
+
+ # Create new assignments
+ now = DateTime.truncate(DateTime.utc_now(), :second)
+
+ assignments =
+ Enum.map(equipment_ids, fn equipment_id ->
+ %{
+ equipment_id: equipment_id,
+ agent_token_id: organization.default_agent_token_id,
+ inserted_at: now,
+ updated_at: now
+ }
+ end)
+
+ {count, _} = Repo.insert_all(AgentAssignment, assignments)
+ {count, nil}
+ else
+ # No default agent set, delete all assignments
+ equipment_ids =
+ Repo.all(
+ from(e in Equipment, join: s in assoc(e, :site), where: s.organization_id == ^organization_id, select: e.id)
+ )
+
+ Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
+ end
+ end
+
## Authorization
@doc """
diff --git a/lib/towerops/sites.ex b/lib/towerops/sites.ex
index 81311850..0cc66739 100644
--- a/lib/towerops/sites.ex
+++ b/lib/towerops/sites.ex
@@ -5,6 +5,8 @@ defmodule Towerops.Sites do
import Ecto.Query
+ alias Towerops.Agents.AgentAssignment
+ alias Towerops.Equipment.Equipment
alias Towerops.Repo
alias Towerops.Sites.Site
@@ -121,4 +123,65 @@ defmodule Towerops.Sites do
%{site: site, children: children}
end
+
+ ## Bulk Configuration Updates
+
+ @doc """
+ Applies site's SNMP configuration to all equipment at the site.
+
+ This updates all equipment records to use the site's SNMP version and community string,
+ overwriting any existing equipment-level configurations.
+
+ Returns the number of equipment records updated.
+ """
+ def apply_snmp_config_to_all_equipment(site_id) do
+ site = get_site!(site_id)
+
+ Repo.update_all(from(e in Equipment, where: e.site_id == ^site_id),
+ set: [
+ snmp_version: site.snmp_version,
+ snmp_community: site.snmp_community,
+ updated_at: DateTime.truncate(DateTime.utc_now(), :second)
+ ]
+ )
+ end
+
+ @doc """
+ Applies site's agent to all equipment at the site.
+
+ This creates/updates agent assignments for all equipment at the site to use the site's
+ agent, overwriting any existing equipment-level assignments.
+
+ Returns the number of equipment records assigned.
+ """
+ def apply_agent_to_all_equipment(site_id) do
+ site = Repo.get!(Site, site_id)
+
+ # Get all equipment IDs for this site
+ equipment_ids = Repo.all(from(e in Equipment, where: e.site_id == ^site_id, select: e.id))
+
+ if site.agent_token_id do
+ # Delete existing assignments
+ Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
+
+ # Create new assignments
+ now = DateTime.truncate(DateTime.utc_now(), :second)
+
+ assignments =
+ Enum.map(equipment_ids, fn equipment_id ->
+ %{
+ equipment_id: equipment_id,
+ agent_token_id: site.agent_token_id,
+ inserted_at: now,
+ updated_at: now
+ }
+ end)
+
+ {count, _} = Repo.insert_all(AgentAssignment, assignments)
+ {count, nil}
+ else
+ # No agent set, delete all assignments (they will inherit from org or use cloud)
+ Repo.delete_all(from(a in AgentAssignment, where: a.equipment_id in ^equipment_ids))
+ end
+ end
end
diff --git a/lib/towerops_web/live/org/settings_live.ex b/lib/towerops_web/live/org/settings_live.ex
index 88ac4d79..ab8416df 100644
--- a/lib/towerops_web/live/org/settings_live.ex
+++ b/lib/towerops_web/live/org/settings_live.ex
@@ -47,4 +47,18 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
+
+ @impl true
+ def handle_event("apply_snmp_to_all", _params, socket) do
+ {count, _} = Organizations.apply_snmp_config_to_all_equipment(socket.assigns.organization.id)
+
+ {:noreply, put_flash(socket, :info, "Applied SNMP configuration to #{count} equipment records across all sites")}
+ end
+
+ @impl true
+ def handle_event("apply_agent_to_all", _params, socket) do
+ {count, _} = Organizations.apply_agent_to_all_equipment(socket.assigns.organization.id)
+
+ {:noreply, put_flash(socket, :info, "Applied default agent to #{count} equipment records across all sites")}
+ end
end
diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex
index aebd635b..b9dd0f60 100644
--- a/lib/towerops_web/live/org/settings_live.html.heex
+++ b/lib/towerops_web/live/org/settings_live.html.heex
@@ -48,6 +48,25 @@
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
SNMP configuration hierarchy: Equipment > Site > Organization
+
+ <%= if @organization.snmp_community do %>
+
+
+ Force Apply to All Equipment
+
+
+ This will override SNMP settings for ALL equipment across all sites in this organization.
+
+ <.button
+ type="button"
+ phx-click="apply_snmp_to_all"
+ data-confirm="This will replace SNMP settings for ALL equipment in this organization. Are you sure?"
+ variant="danger"
+ >
+ <.icon name="hero-arrow-path" class="h-4 w-4" /> Apply SNMP Config to All Equipment
+
+
+ <% end %>
<%= if @available_agents != [] do %>
@@ -98,6 +117,26 @@
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Agent assignment hierarchy: Equipment > Site > Organization
+
+ <%= if @organization.default_agent_token_id do %>
+
+
+ Force Apply to All Equipment
+
+
+ This will assign the default agent to ALL equipment across all sites in this organization.
+
+ <.button
+ type="button"
+ phx-click="apply_agent_to_all"
+ data-confirm="This will replace agent assignments for ALL equipment in this organization. Are you sure?"
+ variant="danger"
+ >
+ <.icon name="hero-arrow-path" class="h-4 w-4" />
+ Apply Default Agent to All Equipment
+
+
+ <% end %>
<% else %>
diff --git a/lib/towerops_web/live/site_live/form.ex b/lib/towerops_web/live/site_live/form.ex
index 941d221d..b34eb287 100644
--- a/lib/towerops_web/live/site_live/form.ex
+++ b/lib/towerops_web/live/site_live/form.ex
@@ -93,6 +93,20 @@ defmodule ToweropsWeb.SiteLive.Form do
end
end
+ @impl true
+ def handle_event("apply_snmp_to_all", _params, socket) do
+ {count, _} = Sites.apply_snmp_config_to_all_equipment(socket.assigns.site.id)
+
+ {:noreply, put_flash(socket, :info, "Applied SNMP configuration to #{count} equipment records at this site")}
+ end
+
+ @impl true
+ def handle_event("apply_agent_to_all", _params, socket) do
+ {count, _} = Sites.apply_agent_to_all_equipment(socket.assigns.site.id)
+
+ {:noreply, put_flash(socket, :info, "Applied agent to #{count} equipment records at this site")}
+ end
+
defp save_site(socket, :new, site_params) do
site_params = Map.put(site_params, "organization_id", socket.assigns.organization.id)
diff --git a/lib/towerops_web/live/site_live/form.html.heex b/lib/towerops_web/live/site_live/form.html.heex
index fb8b9a0e..ef1d119f 100644
--- a/lib/towerops_web/live/site_live/form.html.heex
+++ b/lib/towerops_web/live/site_live/form.html.heex
@@ -40,32 +40,59 @@
<.input field={@form[:location]} type="text" label="Location" />
+ <.input field={@form[:description]} type="textarea" label="Description" />
+
<%= if @available_agents != [] do %>
- <.input
- field={@form[:agent_token_id]}
- type="select"
- label="Default Agent for Site"
- prompt="Inherit from organization"
- options={Enum.map(@available_agents, &{&1.name, &1.id})}
- />
- <%= if @live_action == :edit and @site.agent_token_id do %>
-
- <.icon name="hero-exclamation-triangle" class="h-4 w-4" />
- Overriding organization default -
- all equipment at this site will use this agent
+
+
Agent Configuration (Optional)
+
+ Set a default agent for SNMP polling at this site. This will override the organization default for all equipment at this site. Leave blank to inherit from organization.
- <% else %>
- <%= if Map.get(assigns, :org_agent) do %>
-
- <.icon name="hero-building-office-2" class="h-4 w-4" />
- Currently inheriting organization default: {@org_agent.name}
+
+ <.input
+ field={@form[:agent_token_id]}
+ type="select"
+ label="Default Agent for Site"
+ prompt="Inherit from organization"
+ options={Enum.map(@available_agents, &{&1.name, &1.id})}
+ />
+
+ <%= if @live_action == :edit and @site.agent_token_id do %>
+
+ <.icon name="hero-exclamation-triangle" class="h-4 w-4" />
+ Overriding organization default -
+ all equipment at this site will use this agent
+
+
+
+ Force Apply to Site Equipment
+
+
+ This will assign this agent to ALL equipment at this site.
+
+ <.button
+ type="button"
+ phx-click="apply_agent_to_all"
+ data-confirm="This will replace agent assignments for ALL equipment at this site. Are you sure?"
+ variant="danger"
+ >
+ <.icon name="hero-arrow-path" class="h-4 w-4" /> Apply Agent to All Equipment
+
+
<% else %>
-
- Set a default agent for all equipment at this site. Equipment can override this setting individually.
-
+ <%= if Map.get(assigns, :org_agent) do %>
+
+ <.icon name="hero-building-office-2" class="h-4 w-4" />
+ Currently inheriting organization default: {@org_agent.name}
+
+ <% else %>
+
+ Set a default agent for all equipment at this site. Equipment can override this setting individually.
+
+ <% end %>
<% end %>
- <% end %>
+
<% end %>
@@ -88,9 +115,26 @@
label="SNMP Community String"
placeholder="Leave blank to inherit from organization"
/>
-
- <.input field={@form[:description]} type="textarea" label="Description" />
+ <%= if @live_action == :edit and @site.snmp_community do %>
+
+
+ Force Apply to Site Equipment
+
+
+ This will override SNMP settings for ALL equipment at this site.
+
+ <.button
+ type="button"
+ phx-click="apply_snmp_to_all"
+ data-confirm="This will replace SNMP settings for ALL equipment at this site. Are you sure?"
+ variant="danger"
+ >
+ <.icon name="hero-arrow-path" class="h-4 w-4" /> Apply SNMP Config to All Equipment
+
+
+ <% end %>
+
<.button phx-disable-with="Saving..." variant="primary">Save Site