- Fix broken route paths in dashboard (/discovery, /subscribers/trace, /config-changes) - Fix insights empty state settings link (org-scoped route) - Add underscores to all 86400 literals across 6 files - Alphabetize aliases in search.ex and agent_channel.ex - Extract changelog parser helper to reduce nesting - Extract dashboard impact calculation to reduce nesting - Refactor agent_channel config change detection (pattern match, extract function) - Combine double Enum.reject into single call in trace.ex - Fix line length in trace.ex search query - Replace length/1 > 0 with != [] in trace_live - Run mix format on all files
175 lines
5.1 KiB
Elixir
175 lines
5.1 KiB
Elixir
defmodule Towerops.ConfigChanges do
|
|
@moduledoc """
|
|
Context for managing config change events — linking MikroTik backup diffs
|
|
to time windows for performance correlation analysis.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.ConfigChanges.ConfigChangeEvent
|
|
alias Towerops.Devices.MikrotikBackups
|
|
alias Towerops.Devices.MikrotikBackups.Differ
|
|
alias Towerops.Repo
|
|
|
|
require Logger
|
|
|
|
@mikrotik_sections %{
|
|
"firewall" => ~r/^\/(ip )?firewall/m,
|
|
"interface" => ~r/^\/interface/m,
|
|
"routing" => ~r/^\/(ip |routing )(route|ospf|bgp|rip)/m,
|
|
"wireless" => ~r/^\/interface wireless/m,
|
|
"queue" => ~r/^\/queue/m,
|
|
"nat" => ~r/^\/(ip )?firewall nat/m,
|
|
"address" => ~r/^\/ip address/m,
|
|
"dns" => ~r/^\/ip dns/m,
|
|
"dhcp" => ~r/^\/ip dhcp/m,
|
|
"bridge" => ~r/^\/interface bridge/m,
|
|
"vlan" => ~r/^\/interface vlan/m,
|
|
"snmp" => ~r/^\/snmp/m,
|
|
"user" => ~r/^\/user/m,
|
|
"system" => ~r/^\/system/m,
|
|
"ppp" => ~r/^\/ppp/m
|
|
}
|
|
|
|
@doc """
|
|
Records a config change event from two backup records.
|
|
|
|
Generates a diff, detects changed sections, and stores the event.
|
|
"""
|
|
def record_change_event(device_id, before_backup, after_backup) do
|
|
config_before =
|
|
before_backup.config_compressed
|
|
|> MikrotikBackups.decompress_config()
|
|
|> Differ.mask_sensitive_data()
|
|
|
|
config_after =
|
|
after_backup.config_compressed
|
|
|> MikrotikBackups.decompress_config()
|
|
|> Differ.mask_sensitive_data()
|
|
|
|
case Differ.generate_unified_diff(config_before, config_after) do
|
|
{:ok, ""} ->
|
|
{:ok, :no_changes}
|
|
|
|
{:ok, diff} ->
|
|
stats = Differ.calculate_diff_stats(diff)
|
|
sections = detect_changed_sections(diff)
|
|
|
|
# Truncate diff_summary to first 2000 chars for storage
|
|
summary = String.slice(diff, 0, 2000)
|
|
|
|
device = Repo.get!(Towerops.Devices.Device, device_id)
|
|
|
|
attrs = %{
|
|
device_id: device_id,
|
|
organization_id: device.organization_id,
|
|
backup_before_id: before_backup.id,
|
|
backup_after_id: after_backup.id,
|
|
changed_at: after_backup.backed_up_at,
|
|
diff_summary: summary,
|
|
sections_changed: sections,
|
|
change_size: stats.additions + stats.deletions
|
|
}
|
|
|
|
%ConfigChangeEvent{}
|
|
|> ConfigChangeEvent.changeset(attrs)
|
|
|> Repo.insert()
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to generate diff for config change event: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Lists config change events for a device, most recent first.
|
|
Supports `:after` and `:before` date range filters and `:limit`.
|
|
"""
|
|
def list_device_changes(device_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 50)
|
|
|
|
ConfigChangeEvent
|
|
|> where(device_id: ^device_id)
|
|
|> apply_date_filters(opts)
|
|
|> order_by(desc: :changed_at)
|
|
|> limit(^limit)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc """
|
|
Lists config change events for an organization, most recent first.
|
|
Supports `:after` and `:before` date range filters, `:limit`, and `:preload`.
|
|
"""
|
|
def list_org_changes(organization_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 50)
|
|
preloads = Keyword.get(opts, :preload, [])
|
|
|
|
ConfigChangeEvent
|
|
|> where(organization_id: ^organization_id)
|
|
|> apply_date_filters(opts)
|
|
|> order_by(desc: :changed_at)
|
|
|> limit(^limit)
|
|
|> Repo.all()
|
|
|> Repo.preload(preloads)
|
|
end
|
|
|
|
@doc """
|
|
Lists config change events for devices at a specific site, most recent first.
|
|
Supports `:after`, `:before`, `:limit`, and `:preload` options.
|
|
"""
|
|
def list_site_changes(site_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 20)
|
|
preloads = Keyword.get(opts, :preload, [:device])
|
|
|
|
device_ids =
|
|
site_id
|
|
|> Towerops.Devices.list_site_devices()
|
|
|> Enum.map(& &1.id)
|
|
|
|
if device_ids == [] do
|
|
[]
|
|
else
|
|
ConfigChangeEvent
|
|
|> where([e], e.device_id in ^device_ids)
|
|
|> apply_date_filters(opts)
|
|
|> order_by(desc: :changed_at)
|
|
|> limit(^limit)
|
|
|> Repo.all()
|
|
|> Repo.preload(preloads)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets a single config change event by ID.
|
|
"""
|
|
def get_change_event!(id), do: Repo.get!(ConfigChangeEvent, id)
|
|
|
|
@doc """
|
|
Gets a config change event with preloaded associations.
|
|
"""
|
|
def get_change_event_with_preloads!(id) do
|
|
ConfigChangeEvent
|
|
|> Repo.get!(id)
|
|
|> Repo.preload([:device, :backup_before, :backup_after])
|
|
end
|
|
|
|
# Detect which MikroTik config sections were modified based on diff content
|
|
defp detect_changed_sections(diff) do
|
|
@mikrotik_sections
|
|
|> Enum.filter(fn {_name, pattern} -> Regex.match?(pattern, diff) end)
|
|
|> Enum.map(fn {name, _} -> name end)
|
|
|> Enum.sort()
|
|
end
|
|
|
|
defp apply_date_filters(query, opts) do
|
|
query
|
|
|> maybe_filter_after(Keyword.get(opts, :after))
|
|
|> maybe_filter_before(Keyword.get(opts, :before))
|
|
end
|
|
|
|
defp maybe_filter_after(query, nil), do: query
|
|
defp maybe_filter_after(query, dt), do: where(query, [e], e.changed_at >= ^dt)
|
|
|
|
defp maybe_filter_before(query, nil), do: query
|
|
defp maybe_filter_before(query, dt), do: where(query, [e], e.changed_at <= ^dt)
|
|
end
|