128 lines
3.5 KiB
Elixir
128 lines
3.5 KiB
Elixir
defmodule Towerops.Devices.MikrotikBackups do
|
|
@moduledoc """
|
|
Context for managing MikroTik device configuration backups.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Devices.MikrotikBackup
|
|
alias Towerops.Repo
|
|
|
|
@doc """
|
|
Compresses config text using gzip.
|
|
"""
|
|
def compress_config(config_text) when is_binary(config_text) do
|
|
:zlib.compress(config_text)
|
|
end
|
|
|
|
@doc """
|
|
Decompresses config from binary.
|
|
"""
|
|
def decompress_config(compressed_binary) when is_binary(compressed_binary) do
|
|
:zlib.uncompress(compressed_binary)
|
|
end
|
|
|
|
@doc """
|
|
Normalizes config by removing comments and whitespace.
|
|
This prevents false positives from RouterOS timestamp comments.
|
|
"""
|
|
def normalize_config(config_text) when is_binary(config_text) do
|
|
config_text
|
|
|> String.split("\n")
|
|
|> Enum.reject(&String.starts_with?(&1, "#"))
|
|
|> Enum.map(&String.trim/1)
|
|
|> Enum.reject(&(&1 == ""))
|
|
|> Enum.join("\n")
|
|
end
|
|
|
|
@doc """
|
|
Calculates SHA256 hash of raw config text.
|
|
Used for integrity verification.
|
|
"""
|
|
def calculate_hash(config_text) when is_binary(config_text) do
|
|
:sha256 |> :crypto.hash(config_text) |> Base.encode16(case: :lower)
|
|
end
|
|
|
|
@doc """
|
|
Calculates SHA256 hash of normalized config.
|
|
Used for deduplication - ignores timestamp comments.
|
|
"""
|
|
def calculate_hash_normalized(config_text) when is_binary(config_text) do
|
|
config_text
|
|
|> normalize_config()
|
|
|> then(&:crypto.hash(:sha256, &1))
|
|
|> Base.encode16(case: :lower)
|
|
end
|
|
|
|
@doc """
|
|
Creates a new backup for a device.
|
|
Compresses config and calculates both raw and normalized hashes.
|
|
"""
|
|
def create_backup(device_id, config_text, trigger_source \\ "daily_cron") do
|
|
config_size = byte_size(config_text)
|
|
compressed = compress_config(config_text)
|
|
compressed_size = byte_size(compressed)
|
|
|
|
attrs = %{
|
|
device_id: device_id,
|
|
config_compressed: compressed,
|
|
config_hash: calculate_hash(config_text),
|
|
config_hash_normalized: calculate_hash_normalized(config_text),
|
|
config_size_bytes: config_size,
|
|
compressed_size_bytes: compressed_size,
|
|
backed_up_at: DateTime.utc_now(),
|
|
trigger_source: trigger_source
|
|
}
|
|
|
|
%MikrotikBackup{}
|
|
|> MikrotikBackup.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
@doc """
|
|
Gets the most recent backup for a device.
|
|
Returns nil if no backups exist.
|
|
"""
|
|
def get_latest_backup(device_id) do
|
|
Repo.one(from(b in MikrotikBackup, where: b.device_id == ^device_id, order_by: [desc: b.backed_up_at], limit: 1))
|
|
end
|
|
|
|
@doc """
|
|
Checks if a device needs a backup by comparing normalized hashes.
|
|
Returns {:ok, true} if backup needed, {:ok, false} if config unchanged.
|
|
"""
|
|
def needs_backup?(device_id, config_text) do
|
|
new_hash_normalized = calculate_hash_normalized(config_text)
|
|
|
|
case get_latest_backup(device_id) do
|
|
nil ->
|
|
{:ok, true}
|
|
|
|
latest ->
|
|
{:ok, latest.config_hash_normalized != new_hash_normalized}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Lists backup history for a device, most recent first.
|
|
"""
|
|
def list_device_backups(device_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 100)
|
|
|
|
Repo.all(from(b in MikrotikBackup, where: b.device_id == ^device_id, order_by: [desc: b.backed_up_at], limit: ^limit))
|
|
end
|
|
|
|
@doc """
|
|
Gets a backup by id. Raises if not found.
|
|
"""
|
|
def get_backup!(id) do
|
|
Repo.get!(MikrotikBackup, id)
|
|
end
|
|
|
|
@doc """
|
|
Deletes a backup.
|
|
"""
|
|
def delete_backup(%MikrotikBackup{} = backup) do
|
|
Repo.delete(backup)
|
|
end
|
|
end
|