allow override of agent and snmp community

This commit is contained in:
Graham McIntire 2026-01-17 12:05:08 -06:00
parent 7975581da2
commit dca90db137
No known key found for this signature in database
6 changed files with 268 additions and 23 deletions

View file

@ -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 """

View file

@ -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

View file

@ -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

View file

@ -48,6 +48,25 @@
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
SNMP configuration hierarchy: Equipment > Site > Organization
</p>
<%= if @organization.snmp_community do %>
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to All Equipment
</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
This will override SNMP settings for ALL equipment across all sites in this organization.
</p>
<.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
</.button>
</div>
<% end %>
</div>
<%= 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
</p>
<%= if @organization.default_agent_token_id do %>
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to All Equipment
</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
This will assign the default agent to ALL equipment across all sites in this organization.
</p>
<.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
</.button>
</div>
<% end %>
</div>
<% else %>
<div class="mt-6 border-t pt-6">

View file

@ -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)

View file

@ -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 %>
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>Overriding</strong> organization default -
all equipment at this site will use this agent
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">Agent Configuration (Optional)</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
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.
</p>
<% else %>
<%= if Map.get(assigns, :org_agent) do %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Currently inheriting organization default: <strong>{@org_agent.name}</strong>
<.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 %>
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>Overriding</strong> organization default -
all equipment at this site will use this agent
</p>
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to Site Equipment
</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
This will assign this agent to ALL equipment at this site.
</p>
<.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
</.button>
</div>
<% else %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Set a default agent for all equipment at this site. Equipment can override this setting individually.
</p>
<%= if Map.get(assigns, :org_agent) do %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Currently inheriting organization default: <strong>{@org_agent.name}</strong>
</p>
<% else %>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Set a default agent for all equipment at this site. Equipment can override this setting individually.
</p>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
<div class="mt-6 border-t pt-6">
@ -88,9 +115,26 @@
label="SNMP Community String"
placeholder="Leave blank to inherit from organization"
/>
</div>
<.input field={@form[:description]} type="textarea" label="Description" />
<%= if @live_action == :edit and @site.snmp_community do %>
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to Site Equipment
</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
This will override SNMP settings for ALL equipment at this site.
</p>
<.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
</.button>
</div>
<% end %>
</div>
<div class="flex gap-3 mt-6">
<.button phx-disable-with="Saving..." variant="primary">Save Site</.button>