129 lines
4.3 KiB
Elixir
129 lines
4.3 KiB
Elixir
defmodule Towerops.Uisp.Sync do
|
|
@moduledoc """
|
|
Orchestrates syncing data from the UISP API into the local database.
|
|
|
|
Runs site sync followed by device sync, logs each operation, and updates
|
|
the integration's sync status.
|
|
"""
|
|
|
|
alias Towerops.Integrations
|
|
alias Towerops.Integrations.Integration
|
|
alias Towerops.Repo
|
|
alias Towerops.Uisp.ConfigSnapshot
|
|
alias Towerops.Uisp.DeviceSync
|
|
alias Towerops.Uisp.GpsSync
|
|
alias Towerops.Uisp.SiteSync
|
|
alias Towerops.Uisp.StatisticsSync
|
|
alias Towerops.Uisp.SyncLog
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Main entry point: syncs sites and devices for the given UISP integration.
|
|
|
|
Returns `{:ok, %{sites_synced: count, devices_matched: count, devices_created: count}}`
|
|
or `{:error, reason}`.
|
|
"""
|
|
def sync_organization(%Integration{} = integration) do
|
|
start_time = System.monotonic_time(:millisecond)
|
|
|
|
with {:ok, site_result} <- SiteSync.sync_sites(integration),
|
|
site_map = build_full_site_map(integration, site_result.site_map),
|
|
{:ok, device_result} <- DeviceSync.sync_devices(integration, site_map) do
|
|
# Phase 2: GPS enrichment, statistics sync, config snapshots
|
|
# These are best-effort — failures don't block the main sync
|
|
gps_result = safe_sync(:gps, fn -> GpsSync.sync_gps(device_result.device_pairs || []) end)
|
|
|
|
stats_result =
|
|
safe_sync(:stats, fn -> StatisticsSync.sync_statistics(integration, device_result.device_map || %{}) end)
|
|
|
|
config_result =
|
|
safe_sync(:config, fn -> ConfigSnapshot.sync_configs(integration, device_result.device_map || %{}) end)
|
|
|
|
duration_ms = System.monotonic_time(:millisecond) - start_time
|
|
total_synced = site_result.synced + device_result.matched + device_result.created
|
|
|
|
message =
|
|
"Synced #{site_result.synced} sites, matched #{device_result.matched} devices, created #{device_result.created} devices"
|
|
|
|
log_sync(
|
|
integration,
|
|
"success",
|
|
total_synced,
|
|
%{
|
|
gps: inspect(gps_result),
|
|
stats: inspect(stats_result),
|
|
config: inspect(config_result)
|
|
},
|
|
duration_ms
|
|
)
|
|
|
|
Integrations.update_sync_status(integration, "success", message)
|
|
|
|
{:ok,
|
|
%{
|
|
sites_synced: site_result.synced,
|
|
devices_matched: device_result.matched,
|
|
devices_created: device_result.created
|
|
}}
|
|
else
|
|
{:error, reason} ->
|
|
duration_ms = System.monotonic_time(:millisecond) - start_time
|
|
message = humanize_sync_error(reason)
|
|
log_sync(integration, "failed", 0, %{error: inspect(reason)}, duration_ms)
|
|
Integrations.update_sync_status(integration, "failed", message)
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp build_full_site_map(integration, uisp_id_map) do
|
|
# The uisp_id_map has UISP site IDs -> local sites.
|
|
# Also build a name-based map for any sites already in the DB.
|
|
name_map = SiteSync.build_site_map_by_name(integration.organization_id)
|
|
Map.merge(name_map, uisp_id_map)
|
|
end
|
|
|
|
defp log_sync(integration, status, records_synced, errors, duration_ms) do
|
|
%SyncLog{}
|
|
|> SyncLog.changeset(%{
|
|
organization_id: integration.organization_id,
|
|
integration_id: integration.id,
|
|
status: status,
|
|
records_synced: records_synced,
|
|
errors: errors,
|
|
duration_ms: duration_ms,
|
|
inserted_at: Towerops.Time.now()
|
|
})
|
|
|> Repo.insert()
|
|
end
|
|
|
|
defp safe_sync(label, fun) do
|
|
case fun.() do
|
|
{:ok, result} ->
|
|
result
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("UISP #{label} sync failed: #{inspect(reason)}")
|
|
%{error: reason}
|
|
end
|
|
rescue
|
|
e ->
|
|
Logger.warning("UISP #{label} sync crashed: #{Exception.message(e)}")
|
|
%{error: Exception.message(e)}
|
|
end
|
|
|
|
defp humanize_sync_error(:unauthorized), do: "Authentication failed — check your API key"
|
|
|
|
defp humanize_sync_error(:timeout), do: "Connection timed out — UISP may be temporarily unavailable"
|
|
|
|
defp humanize_sync_error(reason) when is_binary(reason) do
|
|
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
|
|
"SSL certificate verification failed — unable to establish a secure connection"
|
|
else
|
|
Logger.warning("UISP sync failed with raw error: #{reason}")
|
|
"An unexpected error occurred during sync"
|
|
end
|
|
end
|
|
|
|
defp humanize_sync_error(_reason), do: "An unexpected error occurred during sync"
|
|
end
|