towerops/lib/towerops/sites.ex

297 lines
8.3 KiB
Elixir

defmodule Towerops.Sites do
@moduledoc """
The Sites context.
"""
import Ecto.Query
alias Towerops.Agents.AgentAssignment
alias Towerops.Devices.Device
alias Towerops.Repo
alias Towerops.Sites.Site
@doc """
Returns the list of sites for an organization.
Ordered by custom display_order (if set), then alphabetically by name.
"""
def list_organization_sites(organization_id) do
Repo.all(
from(s in Site,
where: s.organization_id == ^organization_id,
order_by: [asc: s.display_order, asc: s.name],
preload: [:parent_site]
)
)
end
@doc """
Returns the count of sites for an organization.
"""
def count_organization_sites(organization_id) do
Repo.aggregate(
from(s in Site, where: s.organization_id == ^organization_id),
:count
)
end
@doc """
Returns the list of root sites (sites without a parent) for an organization.
Ordered by custom display_order (if set), then alphabetically by name.
"""
def list_root_sites(organization_id) do
Repo.all(
from(s in Site,
where: s.organization_id == ^organization_id,
where: is_nil(s.parent_site_id),
order_by: [asc: s.display_order, asc: s.name]
)
)
end
@doc """
Returns the list of child sites for a parent site.
Ordered by custom display_order (if set), then alphabetically by name.
"""
def list_child_sites(parent_site_id) do
Repo.all(
from(s in Site,
where: s.parent_site_id == ^parent_site_id,
order_by: [asc: s.display_order, asc: s.name]
)
)
end
@doc """
Gets a single site.
"""
def get_site!(id) do
Site
|> Repo.get!(id)
|> Repo.preload([:parent_site, :child_sites, :device])
end
@doc """
Gets a single site. Returns nil if not found.
"""
def get_site(id) do
case Repo.get(Site, id) do
nil -> nil
site -> Repo.preload(site, [:parent_site, :child_sites, :device])
end
end
@doc """
Gets a single site belonging to an organization.
"""
def get_organization_site!(organization_id, site_id) do
Repo.one!(
from(s in Site,
where: s.id == ^site_id,
where: s.organization_id == ^organization_id,
preload: [:parent_site, :child_sites, :device]
)
)
end
@doc """
Creates a site.
"""
def create_site(attrs) do
%Site{}
|> Site.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a site.
"""
def update_site(%Site{} = site, attrs) do
old_community = site.snmp_community
old_mikrotik_username = site.mikrotik_username
old_mikrotik_password = site.mikrotik_password
old_mikrotik_port = site.mikrotik_port
old_mikrotik_use_ssl = site.mikrotik_use_ssl
old_mikrotik_enabled = site.mikrotik_enabled
case site
|> Site.changeset(attrs)
|> Repo.update() do
{:ok, updated_site} = result ->
# If community string changed, propagate to inheriting devices
if updated_site.snmp_community != old_community do
Towerops.Devices.propagate_site_community_change(updated_site.id, updated_site.snmp_community)
end
# If any MikroTik settings changed, propagate to inheriting devices
mikrotik_changed =
updated_site.mikrotik_username != old_mikrotik_username ||
updated_site.mikrotik_password != old_mikrotik_password ||
updated_site.mikrotik_port != old_mikrotik_port ||
updated_site.mikrotik_use_ssl != old_mikrotik_use_ssl ||
updated_site.mikrotik_enabled != old_mikrotik_enabled
if mikrotik_changed do
mikrotik_attrs = %{
mikrotik_username: updated_site.mikrotik_username,
mikrotik_password: updated_site.mikrotik_password,
mikrotik_port: updated_site.mikrotik_port,
mikrotik_use_ssl: updated_site.mikrotik_use_ssl,
mikrotik_enabled: updated_site.mikrotik_enabled
}
Towerops.Devices.propagate_site_mikrotik_change(updated_site.id, mikrotik_attrs)
end
result
error ->
error
end
end
@doc """
Deletes a site.
"""
def delete_site(%Site{} = site) do
Repo.delete(site)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking site changes.
"""
def change_site(%Site{} = site, attrs \\ %{}) do
Site.changeset(site, attrs)
end
@doc """
Builds a hierarchical tree structure of sites.
"""
def build_site_tree(organization_id) do
sites = list_organization_sites(organization_id)
root_sites = Enum.filter(sites, &is_nil(&1.parent_site_id))
Enum.map(root_sites, fn root ->
build_tree_node(root, sites)
end)
end
defp build_tree_node(site, all_sites) do
children =
all_sites
|> Enum.filter(&(&1.parent_site_id == site.id))
|> Enum.map(&build_tree_node(&1, all_sites))
%{site: site, children: children}
end
## Bulk Configuration Updates
@doc """
Applies site's SNMP configuration to all devices at the site.
This updates all devices records to use the site's SNMP version and community string,
overwriting any existing device-level configurations.
Returns the number of device records updated.
"""
def apply_snmp_config_to_all_equipment(site_id) do
site = get_site!(site_id)
Repo.update_all(from(e in Device, 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 devices at the site.
This creates/updates agent assignments for all devices at the site to use the site's
agent, overwriting any existing device-level assignments.
Returns the number of device records assigned.
"""
def apply_agent_to_all_equipment(site_id) do
site = Repo.get!(Site, site_id)
# device IDs for this site
device_ids = Repo.all(from(e in Device, 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.device_id in ^device_ids))
# Create new assignments
now = DateTime.truncate(DateTime.utc_now(), :second)
assignments =
Enum.map(device_ids, fn device_id ->
%{
device_id: device_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.device_id in ^device_ids))
end
end
## Display Order Management
@doc """
Reorders a site to a new position within the organization.
Updates display_order for all sites to maintain continuous ordering (1, 2, 3, ...).
The new_position is 1-based (1 = first position).
Returns {:ok, site} on success, {:error, changeset} on failure.
"""
def reorder_site(site_id, new_position) when is_integer(new_position) and new_position > 0 do
Repo.transaction(fn ->
site = Repo.get!(Site, site_id)
organization_id = site.organization_id
# Get all sites excluding the one being moved, in current order
other_sites =
Repo.all(
from(s in Site,
where: s.organization_id == ^organization_id and s.id != ^site_id,
order_by: [asc: s.display_order, asc: s.name]
)
)
# Insert at new position (1-based index, convert to 0-based for List.insert_at)
new_order = List.insert_at(other_sites, new_position - 1, site)
# Update display_order for all sites
new_order
|> Enum.with_index(1)
|> Enum.each(fn {s, order} ->
Repo.update!(Ecto.Changeset.change(s, display_order: order))
end)
Repo.get!(Site, site_id)
end)
end
@doc """
Reset all sites in organization to alphabetical order.
Clears display_order field for all sites, allowing them to fall back to alphabetical sorting.
"""
def reset_site_order(organization_id) do
Repo.update_all(
from(s in Site, where: s.organization_id == ^organization_id),
set: [display_order: nil, updated_at: DateTime.truncate(DateTime.utc_now(), :second)]
)
end
end