- 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
182 lines
5.1 KiB
Elixir
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
|