Added JobCleanupTask that runs on production startup to cancel all existing polling jobs and reschedule them with the latest worker code. This ensures: - No stale jobs run with old code after deployment (e.g., missing SNMPv3 credentials) - All devices get fresh polling jobs with current logic - Production deployments are seamless without manual intervention The task: - Only runs in production environment (skipped in dev/test) - Runs asynchronously after supervisor initialization - Cancels all DevicePollerWorker and DeviceMonitorWorker jobs - Reschedules polling for all SNMP-enabled devices - Is idempotent and safe to run multiple times Removed debug logging from Client and DevicePollerWorker as SNMPv3 implementation is now verified and working correctly.
248 lines
8.1 KiB
Elixir
248 lines
8.1 KiB
Elixir
defmodule Towerops.Application do
|
|
@moduledoc """
|
|
OTP Application module for Towerops.
|
|
|
|
Supervises the Repo, PubSub, Telemetry, Endpoint, and monitoring workers.
|
|
"""
|
|
|
|
use Application
|
|
|
|
require Logger
|
|
|
|
# Capture the build timestamp at compile time (fallback for development)
|
|
@build_timestamp DateTime.utc_now()
|
|
|
|
@impl true
|
|
def start(_type, _args) do
|
|
# Ensure parsetools is started for MIB parsing (yecc dependency)
|
|
_ = Application.ensure_all_started(:parsetools)
|
|
|
|
# Add snmpkit ebin directory to code path for mib_grammar_elixir.beam
|
|
# This is needed because snmpkit has vendored Erlang modules (compiled from .yrl)
|
|
# Note: With pre-compiled MIBs, this may no longer be needed
|
|
snmpkit_ebin = Application.app_dir(:towerops, "../snmpkit/ebin")
|
|
|
|
_ =
|
|
if File.dir?(snmpkit_ebin) do
|
|
:code.add_patha(to_charlist(snmpkit_ebin))
|
|
end
|
|
|
|
# Run migrations on startup (Ecto handles locking for concurrent runs)
|
|
_ =
|
|
if Application.get_env(:towerops, Towerops.Repo)[:database] != "towerops_test" do
|
|
Towerops.Release.migrate()
|
|
end
|
|
|
|
# Configure MIB directories for NIF resolution (before supervisor starts)
|
|
Logger.info("Configuring MIB directories for NIF resolution...")
|
|
mib_dir = Application.app_dir(:towerops, "priv/mibs")
|
|
|
|
# Build list of all MIB directories (including subdirectories for vendor MIBs)
|
|
mib_dirs =
|
|
if File.dir?(mib_dir) do
|
|
# Get main directory plus all subdirectories
|
|
subdirs =
|
|
mib_dir
|
|
|> File.ls!()
|
|
|> Enum.map(&Path.join(mib_dir, &1))
|
|
|> Enum.filter(&File.dir?/1)
|
|
|
|
[mib_dir | subdirs]
|
|
else
|
|
[mib_dir]
|
|
end
|
|
|
|
# Set MIBDIRS environment variable for net-snmp (must be set BEFORE init)
|
|
# Join with colon for Unix-style path list
|
|
mibdirs_env = Enum.join(mib_dirs, ":")
|
|
System.put_env("MIBDIRS", mibdirs_env)
|
|
System.put_env("MIBS", "ALL")
|
|
|
|
Logger.info("MIB directories configured: #{length(mib_dirs)} directories")
|
|
|
|
# Add each directory to net-snmp's search path
|
|
Enum.each(mib_dirs, fn dir ->
|
|
:ok = ToweropsNative.load_mib_directory(dir)
|
|
end)
|
|
|
|
# Initialize the MIB library (loads all MIBs into memory)
|
|
case ToweropsNative.init_mib_library() do
|
|
result when result in ["initialized", "already_initialized"] ->
|
|
Logger.info("MIB library initialized: #{result}")
|
|
|
|
other ->
|
|
Logger.warning("Unexpected MIB library init result: #{inspect(other)}")
|
|
end
|
|
|
|
topologies = Application.get_env(:libcluster, :topologies, [])
|
|
|
|
children =
|
|
[
|
|
{Cluster.Supervisor, [topologies, [name: Towerops.ClusterSupervisor]]},
|
|
ToweropsWeb.Telemetry,
|
|
Towerops.Repo,
|
|
# Encryption vault for sensitive data (passwords, API tokens)
|
|
Towerops.Vault,
|
|
# Rate limiting backend (ETS-based, per-node)
|
|
{Hammer.Backend.ETS, [expiry_ms: to_timeout(minute: 10), cleanup_interval_ms: to_timeout(minute: 1)]},
|
|
{Oban, Application.fetch_env!(:towerops, Oban)},
|
|
{DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore},
|
|
pubsub_spec(),
|
|
# MIB name cache for fast profile matching (starts asynchronous pre-resolution)
|
|
Towerops.Profiles.MibCache,
|
|
# Load YAML profiles into ETS cache (depends on MibCache)
|
|
Towerops.Profiles.YamlProfiles,
|
|
# SnmpKit services for SNMP operations (used for SNMP protocol, not MIB resolution)
|
|
SnmpKit.SnmpMgr.Config,
|
|
SnmpKit.SnmpMgr.MIB
|
|
] ++
|
|
dev_only_workers() ++
|
|
background_workers() ++
|
|
[
|
|
# Start to serve requests, typically the last entry
|
|
ToweropsWeb.Endpoint
|
|
]
|
|
|
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
|
# for other strategies and supported options
|
|
opts = [strategy: :one_for_one, name: Towerops.Supervisor]
|
|
result = Supervisor.start_link(children, opts)
|
|
|
|
# Run post-startup cleanup tasks (production only)
|
|
# This ensures all polling jobs use the latest worker code after deployment
|
|
Task.start(fn ->
|
|
# Wait for supervisor to fully initialize
|
|
Process.sleep(2000)
|
|
Towerops.Workers.JobCleanupTask.run()
|
|
end)
|
|
|
|
result
|
|
end
|
|
|
|
# Tell Phoenix to update the endpoint configuration
|
|
# whenever the application is updated.
|
|
@impl true
|
|
def config_change(changed, _new, removed) do
|
|
ToweropsWeb.Endpoint.config_change(changed, removed)
|
|
:ok
|
|
end
|
|
|
|
# Configure background workers - don't start in test environment
|
|
defp background_workers do
|
|
if Application.get_env(:towerops, :env) == :test do
|
|
[]
|
|
else
|
|
[
|
|
# Start event logger (subscribes to PubSub)
|
|
Towerops.Devices.EventLogger
|
|
# Note: NeighborCleanupWorker, StaleAgentWorker, AgentLatencyEvaluator,
|
|
# and JobHealthCheckWorker are now Oban Cron jobs (see config/runtime.exs)
|
|
]
|
|
end
|
|
end
|
|
|
|
# Configure dev-only workers - profile watcher for auto-reload
|
|
defp dev_only_workers do
|
|
if Application.get_env(:towerops, :env) == :dev do
|
|
[
|
|
# Watch YAML profiles for changes and auto-reload
|
|
Towerops.Profiles.ProfileWatcher
|
|
]
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
# Returns PubSub spec - uses Redis in production/clustered environments,
|
|
# falls back to default PG2 adapter for Mix tasks and development
|
|
defp pubsub_spec do
|
|
redis_config = Application.get_env(:towerops, :redis, [])
|
|
node_name = node()
|
|
|
|
# Use Redis adapter only when:
|
|
# 1. Redis config exists with host
|
|
# 2. Node has a name (not nonode@nohost)
|
|
use_redis? =
|
|
Keyword.has_key?(redis_config, :host) &&
|
|
node_name != :nonode@nohost
|
|
|
|
if use_redis? do
|
|
base_config = [name: Towerops.PubSub, adapter: Phoenix.PubSub.Redis]
|
|
redis_opts = redis_pubsub_config()
|
|
{Phoenix.PubSub, Keyword.merge(base_config, redis_opts)}
|
|
else
|
|
# Default PG2 adapter for Mix tasks and development
|
|
{Phoenix.PubSub, name: Towerops.PubSub}
|
|
end
|
|
end
|
|
|
|
defp redis_pubsub_config do
|
|
redis_config = Application.get_env(:towerops, :redis, [])
|
|
|
|
# Base configuration for Phoenix.PubSub.Redis
|
|
base_opts = [
|
|
host: Keyword.get(redis_config, :host, "localhost"),
|
|
port: Keyword.get(redis_config, :port, 6379),
|
|
node_name: node()
|
|
]
|
|
|
|
# Add password if configured (read from application config set by runtime.exs)
|
|
base_opts =
|
|
case Keyword.get(redis_config, :password) do
|
|
nil -> base_opts
|
|
"" -> base_opts
|
|
password -> Keyword.put(base_opts, :password, password)
|
|
end
|
|
|
|
# Add resilience options for better reconnection handling
|
|
resilience_opts = [
|
|
# Redix connection options for better resilience
|
|
socket_opts: [
|
|
# TCP keepalive to detect dead connections
|
|
keepalive: true
|
|
],
|
|
# Connection timeout (5 seconds)
|
|
timeout: 5_000,
|
|
# Backoff settings for reconnection
|
|
backoff_initial: 500,
|
|
backoff_max: 30_000,
|
|
# Don't exit process on disconnection - let PubSub handle it
|
|
exit_on_disconnection: false,
|
|
# Sync connect to fail fast if Redis unavailable at startup
|
|
sync_connect: false
|
|
]
|
|
|
|
Keyword.merge(base_opts, resilience_opts)
|
|
end
|
|
|
|
@doc """
|
|
Returns the deployment timestamp.
|
|
|
|
In production (Kubernetes), this reads from the DEPLOY_TIMESTAMP environment
|
|
variable which is automatically set by GitLab CI during deployment. Falls back
|
|
to compile time in development.
|
|
|
|
The timestamp is set by GitLab CI using:
|
|
```bash
|
|
kubectl set env deployment/towerops DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -n towerops
|
|
```
|
|
|
|
This ensures all pods in the deployment show the same deployment time, regardless
|
|
of when individual pods were created or restarted.
|
|
"""
|
|
@spec build_timestamp() :: DateTime.t()
|
|
def build_timestamp do
|
|
case System.get_env("DEPLOY_TIMESTAMP") do
|
|
nil ->
|
|
# Development/fallback: use compile time
|
|
@build_timestamp
|
|
|
|
timestamp_string ->
|
|
# Production: parse from environment variable
|
|
case DateTime.from_iso8601(timestamp_string) do
|
|
{:ok, datetime, _offset} -> datetime
|
|
{:error, _} -> @build_timestamp
|
|
end
|
|
end
|
|
end
|
|
end
|