Prefix fire-and-forget calls (PubSub.broadcast, Oban enqueue, Logger, update_*, etc.) with `_ =` so dialyzer knows the return value is intentionally discarded. Wrap a few if/case expressions whose branches produce mixed types the same way. No behavior changes — only explicit acknowledgement of discarded returns. Warnings: 242 → 88.
150 lines
4.7 KiB
Elixir
150 lines
4.7 KiB
Elixir
defmodule Towerops.Workers.MikrotikBackupWorker do
|
|
@moduledoc """
|
|
Oban worker that triggers daily MikroTik configuration backups.
|
|
|
|
Runs daily at 3 AM via cron schedule. For each eligible device, sends a backup
|
|
job to the assigned agent and creates a tracking request.
|
|
|
|
Eligible devices:
|
|
- Have MikroTik detected (manufacturer contains "MikroTik" or "RouterOS")
|
|
- Have MikroTik API enabled
|
|
- Have credentials configured (username present)
|
|
- Are assigned to an agent
|
|
"""
|
|
|
|
use Oban.Worker, queue: :maintenance, max_attempts: 3
|
|
|
|
alias Towerops.Agent.AgentJob
|
|
alias Towerops.Agent.MikrotikCommand
|
|
alias Towerops.Agent.MikrotikDevice
|
|
alias Towerops.Devices
|
|
alias Towerops.Devices.BackupRequests
|
|
|
|
require Logger
|
|
|
|
@impl Oban.Worker
|
|
@spec perform(Oban.Job.t()) :: :ok | {:error, String.t()}
|
|
def perform(%Oban.Job{}) do
|
|
Logger.info("Starting MikroTik configuration backup job")
|
|
|
|
devices = list_backup_eligible_devices()
|
|
Logger.info("Found #{length(devices)} MikroTik devices to backup")
|
|
|
|
results = Enum.map(devices, &backup_device/1)
|
|
|
|
success_count = Enum.count(results, &match?({:ok, _}, &1))
|
|
error_count = Enum.count(results, &match?({:error, _}, &1))
|
|
skipped_count = Enum.count(results, &match?({:skipped, _}, &1))
|
|
|
|
Logger.info("Backup job complete",
|
|
success: success_count,
|
|
errors: error_count,
|
|
skipped: skipped_count
|
|
)
|
|
|
|
if error_count > 0, do: {:error, "#{error_count} backups failed"}, else: :ok
|
|
end
|
|
|
|
defp list_backup_eligible_devices do
|
|
# Query all devices with:
|
|
# - MikroTik detected (manufacturer contains "MikroTik" or "RouterOS")
|
|
# - MikroTik API enabled
|
|
# - Has credentials (username present)
|
|
# - Assigned to an agent
|
|
Devices.list_mikrotik_devices_with_api()
|
|
end
|
|
|
|
defp backup_device(device) do
|
|
# Resolve which agent this device should use (hierarchical: device -> site -> org -> global)
|
|
agent_token_id = Devices.resolve_agent_token_id(device)
|
|
|
|
if is_nil(agent_token_id) do
|
|
Logger.warning("Device has no agent assigned, skipping backup", device_id: device.id)
|
|
{:skipped, :no_agent}
|
|
else
|
|
# Build backup job
|
|
job_id = "backup:#{device.id}:#{DateTime.to_unix(DateTime.utc_now())}"
|
|
|
|
mikrotik_config = Devices.get_mikrotik_config(device)
|
|
|
|
# Generate unique filename for this backup (without .rsc extension, RouterOS adds it)
|
|
backup_filename = "towerops-backup-#{DateTime.to_unix(DateTime.utc_now())}"
|
|
|
|
# Build chunked read commands
|
|
# Max chunk size is 32KB (32768 bytes), we'll read 10 chunks to cover 320KB
|
|
# Most MikroTik configs are under this size
|
|
chunk_size = 32_768
|
|
num_chunks = 10
|
|
|
|
read_commands =
|
|
for i <- 0..(num_chunks - 1) do
|
|
%MikrotikCommand{
|
|
command: "/file/read",
|
|
args: %{
|
|
"file" => "#{backup_filename}.rsc",
|
|
"offset" => to_string(i * chunk_size),
|
|
"chunk-size" => to_string(chunk_size)
|
|
}
|
|
}
|
|
end
|
|
|
|
job = %AgentJob{
|
|
job_id: job_id,
|
|
job_type: :MIKROTIK,
|
|
device_id: device.id,
|
|
mikrotik_device: %MikrotikDevice{
|
|
ip: device.ip_address,
|
|
username: mikrotik_config.username,
|
|
password: mikrotik_config.password,
|
|
port: mikrotik_config.port,
|
|
ssh_port: mikrotik_config.ssh_port,
|
|
use_ssl: mikrotik_config.use_ssl
|
|
},
|
|
mikrotik_commands:
|
|
[
|
|
# Step 1: Export config to file on router (compact format, no defaults)
|
|
%MikrotikCommand{
|
|
command: "/export",
|
|
args: %{"file" => backup_filename, "compact" => ""}
|
|
}
|
|
] ++
|
|
read_commands ++
|
|
[
|
|
# Final step: Delete the temporary file
|
|
%MikrotikCommand{
|
|
command: "/file/remove",
|
|
args: %{"numbers" => "#{backup_filename}.rsc"}
|
|
}
|
|
]
|
|
}
|
|
|
|
# Create tracking request
|
|
case BackupRequests.create_request(device.id, job_id) do
|
|
{:ok, _request} ->
|
|
# Broadcast to agent via PubSub
|
|
_ =
|
|
Phoenix.PubSub.broadcast(
|
|
Towerops.PubSub,
|
|
"agent:#{agent_token_id}:backup",
|
|
{:backup_requested, job}
|
|
)
|
|
|
|
Logger.info("Backup job sent to agent",
|
|
device_id: device.id,
|
|
job_id: job_id,
|
|
agent_token_id: agent_token_id
|
|
)
|
|
|
|
{:ok, job_id}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to create backup request",
|
|
device_id: device.id,
|
|
reason: inspect(reason)
|
|
)
|
|
|
|
{:error, reason}
|
|
end
|
|
end
|
|
end
|
|
end
|