towerops/lib/towerops/cn_maestro/sync.ex
Graham McIntire 7c8731f86a fix: make sidebar sticky with header and resolve all credo issues (#32)
- Changed sidebar from fixed to sticky positioning
- Wrapped sidebar and main content in flex container
- Sidebar now sticks to top along with header when scrolling
- Removed unused Ecto.Query import from gps_sync.ex

Credo improvements (17 → 0 remaining):
- Replaced all length/1 checks with empty list comparisons
- Extracted helper functions to reduce nesting depth
- Split complex functions into smaller, testable units
- Applied 'with' statements for clearer control flow
- Refactored high-complexity functions (cyclomatic complexity)
- Files improved: storm_detector, alert_notification_worker,
  alert_digest_worker, gps_sync, statistics_sync, device_sync,
  site_sync, site_correlation, reports_live, topology,
  pagerduty/client, cn_maestro/sync

Reviewed-on: graham/towerops-web#32
2026-03-15 18:19:09 -05:00

182 lines
5.1 KiB
Elixir

defmodule Towerops.CnMaestro.Sync do
@moduledoc """
Orchestrates syncing data from cnMaestro into Towerops.
Authenticates via OAuth, then syncs networks (sites) and devices.
"""
import Ecto.Query
alias Towerops.CnMaestro.Client
alias Towerops.Devices.Device
alias Towerops.Integrations
alias Towerops.Integrations.Integration
alias Towerops.Repo
alias Towerops.Sites.Site
require Logger
@doc """
Main entry point: syncs networks and devices for a cnMaestro integration.
"""
def sync_organization(%Integration{} = integration) do
base_url = integration.credentials["url"]
client_id = integration.credentials["client_id"]
client_secret = integration.credentials["client_secret"]
org_id = integration.organization_id
start_time = System.monotonic_time(:millisecond)
with {:ok, token} <- Client.get_token(base_url, client_id, client_secret),
{:ok, network_result} <- sync_networks(base_url, token, org_id),
{:ok, device_result} <- sync_devices(base_url, token, org_id, network_result.site_map) do
duration_ms = System.monotonic_time(:millisecond) - start_time
message =
"Synced #{network_result.synced} networks, matched #{device_result.matched} devices, created #{device_result.created} devices"
Integrations.update_sync_status(integration, "success", message)
Logger.info("cnMaestro sync completed for org #{org_id} in #{duration_ms}ms: #{message}")
{:ok,
%{
networks_synced: network_result.synced,
devices_matched: device_result.matched,
devices_created: device_result.created
}}
else
{:error, :unauthorized} ->
Integrations.update_sync_status(integration, "failed", "Authentication failed — check client credentials")
{:error, :unauthorized}
{:error, reason} ->
Integrations.update_sync_status(integration, "failed", "Sync failed: #{inspect(reason)}")
{:error, reason}
end
end
defp sync_networks(base_url, token, org_id) do
case Client.list_networks(base_url, token) do
{:ok, networks} ->
{synced, site_map} =
Enum.reduce(networks, {0, %{}}, fn network, acc ->
process_network(network, org_id, acc)
end)
{:ok, %{synced: synced, site_map: site_map}}
{:error, reason} ->
{:error, reason}
end
end
defp process_network(network, org_id, {count, acc}) do
name = network["name"]
network_id = network["id"] || network["name"]
if is_nil(name) or name == "" do
{count, acc}
else
upsert_network_site(org_id, name, network_id, count, acc)
end
end
defp upsert_network_site(org_id, name, network_id, count, acc) do
attrs = %{organization_id: org_id, name: name}
case upsert_site(attrs) do
{:ok, site} ->
new_acc = if network_id, do: Map.put(acc, network_id, site), else: acc
{count + 1, new_acc}
{:error, _} ->
{count, acc}
end
end
defp sync_devices(base_url, token, org_id, site_map) do
case Client.list_devices(base_url, token) do
{:ok, devices} ->
result =
Enum.reduce(devices, %{matched: 0, created: 0}, fn device_data, acc ->
process_device(device_data, org_id, site_map, acc)
end)
{:ok, result}
{:error, reason} ->
{:error, reason}
end
end
defp process_device(device_data, org_id, site_map, acc) do
mac = device_data["mac"]
ip = device_data["ip"]
name = device_data["name"] || device_data["mac"]
network = device_data["network"]
local_site = site_map[network]
if is_nil(ip) and is_nil(mac) do
acc
else
sync_single_device(org_id, ip, mac, name, local_site, acc)
end
end
defp sync_single_device(org_id, ip, mac, name, local_site, acc) do
case find_existing_device(org_id, ip, mac) do
nil ->
case create_device(org_id, ip || mac, name, local_site) do
{:ok, _} -> Map.update!(acc, :created, &(&1 + 1))
{:error, _} -> acc
end
device ->
maybe_update_site(device, local_site)
Map.update!(acc, :matched, &(&1 + 1))
end
end
defp find_existing_device(org_id, ip, _mac) when not is_nil(ip) do
Device
|> where(organization_id: ^org_id, ip_address: ^ip)
|> limit(1)
|> Repo.one()
end
defp find_existing_device(_org_id, _ip, _mac), do: nil
defp create_device(org_id, ip, name, site) do
%Device{}
|> Device.changeset(%{
organization_id: org_id,
ip_address: ip,
name: name,
site_id: if(site, do: site.id),
snmp_enabled: true,
monitoring_enabled: true
})
|> Repo.insert()
end
defp maybe_update_site(device, nil), do: {:ok, device}
defp maybe_update_site(device, site) do
if device.site_id == site.id do
{:ok, device}
else
device |> Device.changeset(%{site_id: site.id}) |> Repo.update()
end
end
defp upsert_site(attrs) do
%Site{}
|> Site.changeset(attrs)
|> Repo.insert(
on_conflict: {:replace, [:updated_at]},
conflict_target: [:organization_id, :name],
returning: true
)
end
end