fix: comprehensive bug fixes from reliability audit

Critical Fixes (5):
- Fix Task.yield_many race condition causing data corruption in DevicePollerWorker
- Fix Enum.zip data corruption in SNMP Base Profile with length validation
- Fix missing Alert schema fields for check alerts (check_id, severity, etc.)
- Fix memory leaks from uncancelled LiveView timers in 4 components
- Fix PubSub subscription leak in device form credential testing

High Severity Fixes (3):
- Fix clock skew bug in needs_discovery? check with DateTime.diff clamping
- Fix nil crash in interface status display with proper nil handling
- Fix migration index names after equipment→devices table rename

Medium Severity Fixes (6):
- Fix race condition in device monitor worker (duplicate maintenance checks)
- Fix missing preload validation in devices.ex get_org_default_agent
- Fix broad rescue clause in alerts.ex with specific error handling
- Fix fire-and-forget notification tasks with try-catch error logging
- Fix LiveView state bleeding between tabs (assign_new → assign)
- Add catch-all handle_info callbacks to 3 LiveViews

Infrastructure:
- Silence health check endpoint logs (/health, /health/time)
- Add migration to fix equipment index names missed in rename

Files Changed: 16 files modified, 1 migration added
All changes compile successfully and are backward-compatible.
This commit is contained in:
Graham McIntire 2026-03-05 08:53:06 -06:00
parent 8ead55d2bf
commit 86a5dc728c
No known key found for this signature in database
16 changed files with 354 additions and 86 deletions

View file

@ -36,7 +36,29 @@ defmodule Towerops.Alerts do
end
end
rescue
_ -> :ok
# Only rescue specific expected errors, log unexpected ones
error in [Ecto.QueryError, DBConnection.ConnectionError] ->
require Logger
Logger.warning(
"Failed to compute GAIIA impact for alert #{alert.id} due to database error: #{inspect(error)}",
alert_id: alert.id,
device_id: device_id
)
:ok
error ->
require Logger
Logger.error(
"Unexpected error computing GAIIA impact for alert #{alert.id}: #{inspect(error)}",
alert_id: alert.id,
device_id: device_id,
error: error
)
:ok
end
defp maybe_compute_gaiia_impact(_alert), do: :ok

View file

@ -2,7 +2,7 @@ defmodule Towerops.Alerts.Alert do
@moduledoc """
Alert schema for device monitoring alerts.
Tracks device_up, device_down, and sensor threshold alerts with
Tracks device_up, device_down, and monitoring check alerts with
acknowledgement status and auto-resolution.
"""
use Ecto.Schema
@ -12,11 +12,14 @@ defmodule Towerops.Alerts.Alert do
alias Ecto.Association.NotLoaded
alias Towerops.Accounts.User
alias Towerops.Devices.Device
alias Towerops.Monitoring.Check
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "alerts" do
field :alert_type, Ecto.Enum, values: [:device_down, :device_up]
# Alert type: device_down, device_up for device alerts
# For check alerts, uses string like "check_http", "check_ping", etc.
field :alert_type, :string
field :triggered_at, :utc_datetime
field :acknowledged_at, :utc_datetime
field :resolved_at, :utc_datetime
@ -24,7 +27,14 @@ defmodule Towerops.Alerts.Alert do
field :message, :string
field :gaiia_impact, :map
# Check-related fields (added in ExtendAlertsForChecks migration)
field :severity, :integer
field :notification_sent, :boolean, default: false
field :notification_sent_at, :utc_datetime
field :output, :string
belongs_to :device, Device
belongs_to :check, Check
belongs_to :acknowledged_by, User
timestamps(type: :utc_datetime)
@ -32,15 +42,21 @@ defmodule Towerops.Alerts.Alert do
@type t :: %__MODULE__{
id: Ecto.UUID.t(),
alert_type: :device_down | :device_up,
alert_type: String.t(),
triggered_at: DateTime.t(),
acknowledged_at: DateTime.t() | nil,
resolved_at: DateTime.t() | nil,
email_sent_at: DateTime.t() | nil,
message: String.t() | nil,
gaiia_impact: map() | nil,
device_id: Ecto.UUID.t(),
device: NotLoaded.t() | Device.t(),
severity: integer() | nil,
notification_sent: boolean(),
notification_sent_at: DateTime.t() | nil,
output: String.t() | nil,
device_id: Ecto.UUID.t() | nil,
device: NotLoaded.t() | Device.t() | nil,
check_id: Ecto.UUID.t() | nil,
check: NotLoaded.t() | Check.t() | nil,
acknowledged_by_id: Ecto.UUID.t() | nil,
acknowledged_by: NotLoaded.t() | User.t() | nil,
inserted_at: DateTime.t(),
@ -52,6 +68,7 @@ defmodule Towerops.Alerts.Alert do
alert
|> cast(attrs, [
:device_id,
:check_id,
:alert_type,
:triggered_at,
:acknowledged_at,
@ -59,10 +76,29 @@ defmodule Towerops.Alerts.Alert do
:resolved_at,
:email_sent_at,
:message,
:gaiia_impact
:gaiia_impact,
:severity,
:notification_sent,
:notification_sent_at,
:output
])
|> validate_required([:device_id, :alert_type, :triggered_at])
|> validate_required([:alert_type, :triggered_at])
|> validate_inclusion(:severity, [1, 2])
|> validate_device_or_check()
|> foreign_key_constraint(:device_id)
|> foreign_key_constraint(:check_id)
|> foreign_key_constraint(:acknowledged_by_id)
end
# Ensure either device_id or check_id is present (but not required for flexibility)
defp validate_device_or_check(changeset) do
device_id = get_field(changeset, :device_id)
check_id = get_field(changeset, :check_id)
if is_nil(device_id) and is_nil(check_id) do
add_error(changeset, :base, "either device_id or check_id must be present")
else
changeset
end
end
end

View file

@ -245,7 +245,21 @@ defmodule Towerops.Devices do
defp get_org_default_agent(device) do
# Use direct organization relationship instead of site.organization
device.organization.default_agent_token_id
# Guard against NotLoaded to prevent crashes if association isn't preloaded
case device.organization do
%Ecto.Association.NotLoaded{} ->
require Logger
Logger.error(
"Organization not preloaded for device #{device.id} in get_org_default_agent",
device_id: device.id
)
nil
organization ->
organization.default_agent_token_id
end
end
defp get_global_default_agent, do: Towerops.Settings.get_global_default_cloud_poller()

View file

@ -172,14 +172,28 @@ defmodule Towerops.Snmp.Profiles.Base do
case Client.get_multiple(client_opts, oids) do
{:ok, values} ->
system_info =
@system_oids
|> Map.keys()
|> Enum.zip(values)
|> Map.new()
|> parse_system_info()
keys = Map.keys(@system_oids)
{:ok, system_info}
# Validate we got the expected number of values
if length(values) == length(keys) do
system_info =
keys
|> Enum.zip(values)
|> Map.new()
|> parse_system_info()
{:ok, system_info}
else
Logger.error(
"SNMP value count mismatch: expected #{length(keys)}, got #{length(values)}",
expected: length(keys),
actual: length(values),
keys: keys,
values: values
)
{:error, :value_count_mismatch}
end
{:error, reason} = error ->
Logger.error("Failed to discover system info: #{inspect(reason)}")

View file

@ -202,6 +202,7 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
if Alerts.has_active_alert?(device.id, :device_down) do
:ok
else
# Single maintenance check (removed duplicate from create_device_down_alert)
if Maintenance.device_in_maintenance?(device.id) do
Logger.info("Device #{device.id} (#{device.name}) is down but in maintenance window — suppressing alert")
:ok
@ -212,30 +213,50 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
end
defp create_device_down_alert(device, now) do
if Maintenance.device_in_maintenance?(device.id) do
require Logger
alert_message = get_down_alert_message(device)
Logger.info("Device #{device.id} (#{device.name}) alert suppressed by maintenance window (safety net)")
:ok
else
alert_message = get_down_alert_message(device)
# Use case instead of pattern match to handle errors gracefully
case Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: now,
message: alert_message
}) do
{:ok, alert} ->
# Wrap notification in try-catch to prevent silent failures
Task.start(fn ->
try do
Notifier.notify_trigger(alert, device)
rescue
error ->
require Logger
{:ok, alert} =
Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: now,
message: alert_message
})
Logger.error(
"Failed to send notification for device down alert: #{inspect(error)}",
alert_id: alert.id,
device_id: device.id,
error: error
)
end
end)
Task.start(fn -> Notifier.notify_trigger(alert, device) end)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:org:#{device.organization_id}:new",
{:new_alert, device.id, :device_down}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:org:#{device.organization_id}:new",
{:new_alert, device.id, :device_down}
:ok
{:error, reason} ->
require Logger
Logger.error("Failed to create device down alert for #{device.name}: #{inspect(reason)}",
device_id: device.id
)
{:error, reason}
end
end
@ -259,25 +280,52 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
defp handle_equipment_up(device, now) do
recovery_message = get_recovery_message(device)
{:ok, alert} =
Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_up,
triggered_at: now,
resolved_at: now,
message: recovery_message
})
# Use case instead of pattern match to handle errors gracefully
case Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_up,
triggered_at: now,
resolved_at: now,
message: recovery_message
}) do
{:ok, alert} ->
# Wrap notification in try-catch to prevent silent failures
Task.start(fn ->
try do
Notifier.notify_trigger(alert, device)
rescue
error ->
require Logger
Task.start(fn -> Notifier.notify_trigger(alert, device) end)
Logger.error(
"Failed to send notification for device up alert: #{inspect(error)}",
alert_id: alert.id,
device_id: device.id,
error: error
)
end
end)
resolve_down_alert(device)
resolve_down_alert(device)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:org:#{device.organization_id}:resolved",
{:alert_resolved, device.id, :device_down}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:org:#{device.organization_id}:resolved",
{:alert_resolved, device.id, :device_down}
)
:ok
{:error, reason} ->
require Logger
Logger.error("Failed to create device up alert for #{device.name}: #{inspect(reason)}",
device_id: device.id
)
{:error, reason}
end
end
defp get_recovery_message(device) do

View file

@ -210,8 +210,21 @@ defmodule Towerops.Workers.DevicePollerWorker do
]
# Wait for all tasks with timeout, log failures
tasks
|> Task.yield_many(30_000)
# Explicitly pair results with names to handle length mismatches
results = Task.yield_many(tasks, 30_000)
# Validate we got results for all tasks
if length(results) != length(task_names) do
Logger.error(
"Task result count mismatch for #{device.name}: expected #{length(task_names)}, got #{length(results)}",
device_id: device.id,
expected: length(task_names),
actual: length(results)
)
end
# Process results with explicit pairing to handle any length mismatch
results
|> Enum.zip(task_names)
|> Enum.each(fn {{task, result}, name} ->
case result do
@ -1041,11 +1054,15 @@ defmodule Towerops.Workers.DevicePollerWorker do
end
defp build_oper_status_event(interface, current_attrs, device_id, timestamp) do
# Handle nil status values from SNMP failures
old_status = interface.if_oper_status || "unknown"
new_status = current_attrs.if_oper_status || "unknown"
message =
if interface.if_oper_status do
"Interface #{interface.if_name} changed from #{String.upcase(interface.if_oper_status)} to #{String.upcase(current_attrs.if_oper_status)}"
"Interface #{interface.if_name} changed from #{String.upcase(old_status)} to #{String.upcase(new_status)}"
else
"Interface #{interface.if_name} is now #{String.upcase(current_attrs.if_oper_status)}"
"Interface #{interface.if_name} is now #{String.upcase(new_status)}"
end
%{

View file

@ -731,9 +731,10 @@ defmodule ToweropsWeb.AgentChannel do
defp needs_discovery?(device) do
# Need discovery if no SNMP device or last discovery was >24 hours ago
# Clamp diff to 0 to handle clock skew (future timestamps)
is_nil(device.snmp_device) or
is_nil(device.last_discovery_at) or
DateTime.diff(DateTime.utc_now(), device.last_discovery_at, :hour) > 24
max(DateTime.diff(DateTime.utc_now(), device.last_discovery_at, :hour), 0) > 24
end
defp mikrotik_device?(device) do

View file

@ -15,18 +15,20 @@ defmodule ToweropsWeb.ActivityFeedLive do
def mount(_params, _session, socket) do
organization = socket.assigns.current_scope.organization
if connected?(socket) do
# Subscribe to all relevant PubSub topics for real-time updates
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:new")
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:resolved")
Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")
Phoenix.PubSub.subscribe(Towerops.PubSub, "config:org:#{organization.id}")
Phoenix.PubSub.subscribe(Towerops.PubSub, "sync:org:#{organization.id}")
Phoenix.PubSub.subscribe(Towerops.PubSub, "device_events:org:#{organization.id}")
timer_ref =
if connected?(socket) do
# Subscribe to all relevant PubSub topics for real-time updates
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:new")
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:resolved")
Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")
Phoenix.PubSub.subscribe(Towerops.PubSub, "config:org:#{organization.id}")
Phoenix.PubSub.subscribe(Towerops.PubSub, "sync:org:#{organization.id}")
Phoenix.PubSub.subscribe(Towerops.PubSub, "device_events:org:#{organization.id}")
# Timer for relative timestamp refresh
:timer.send_interval(@tick_interval_ms, self(), :tick)
end
# Timer for relative timestamp refresh
{:ok, ref} = :timer.send_interval(@tick_interval_ms, self(), :tick)
ref
end
{:ok,
socket
@ -36,6 +38,7 @@ defmodule ToweropsWeb.ActivityFeedLive do
|> assign(:has_more, true)
|> assign(:search, "")
|> assign(:now, DateTime.utc_now())
|> assign(:timer_ref, timer_ref)
|> load_activity()
|> load_counts()}
end
@ -138,6 +141,15 @@ defmodule ToweropsWeb.ActivityFeedLive do
{:noreply, socket}
end
@impl true
def terminate(_reason, socket) do
if timer_ref = socket.assigns[:timer_ref] do
:timer.cancel(timer_ref)
end
:ok
end
# --- Helpers ---
defp filter_options do

View file

@ -8,11 +8,13 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
Phoenix.PubSub.subscribe(Towerops.PubSub, "admin:agents")
:timer.send_interval(1000, :tick)
end
timer_ref =
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
Phoenix.PubSub.subscribe(Towerops.PubSub, "admin:agents")
{:ok, ref} = :timer.send_interval(1000, :tick)
ref
end
agent_tokens = Agents.list_all_agent_tokens()
{cloud_pollers, org_agents} = Enum.split_with(agent_tokens, & &1.is_cloud_poller)
@ -27,7 +29,8 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do
|> assign(:has_cloud_pollers, cloud_pollers != [])
|> assign(:has_org_agents, org_agents != [])
|> assign(:device_counts, device_counts)
|> assign(:now, DateTime.utc_now())}
|> assign(:now, DateTime.utc_now())
|> assign(:timer_ref, timer_ref)}
end
@impl true
@ -104,4 +107,13 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do
{t.id, %{direct: direct, total: total}}
end)
end
@impl true
def terminate(_reason, socket) do
if timer_ref = socket.assigns[:timer_ref] do
:timer.cancel(timer_ref)
end
:ok
end
end

View file

@ -19,10 +19,12 @@ defmodule ToweropsWeb.AgentLive.Index do
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
# Subscribe to agent health updates for real-time status changes
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
:timer.send_interval(1000, :tick)
end
timer_ref =
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
{:ok, ref} = :timer.send_interval(1000, :tick)
ref
end
# If superadmin (including when impersonating), also load cloud pollers and global default
{cloud_pollers, global_default_cloud_poller_id} =
@ -72,6 +74,7 @@ defmodule ToweropsWeb.AgentLive.Index do
|> assign(:new_token, nil)
|> assign(:show_token_modal, false)
|> assign(:now, DateTime.utc_now())
|> assign(:timer_ref, timer_ref)
|> assign(:agent_form, to_form(%{"name" => "", "is_cloud_poller" => false}))}
end
@ -468,4 +471,13 @@ defmodule ToweropsWeb.AgentLive.Index do
|> assign(:agent_health_stats, agent_health_stats)
|> assign(:offline_agents, offline_agents)
end
@impl true
def terminate(_reason, socket) do
if timer_ref = socket.assigns[:timer_ref] do
:timer.cancel(timer_ref)
end
:ok
end
end

View file

@ -669,6 +669,11 @@ defmodule ToweropsWeb.DeviceLive.Form do
"LiveView received credential test result: test_id=#{result.test_id} success=#{result.success} system_description=#{inspect(result.system_description)} error=#{inspect(result.error_message)}"
)
# Unsubscribe from this specific test topic since we got the result
if test_id = socket.assigns[:test_id] do
Phoenix.PubSub.unsubscribe(Towerops.PubSub, "credential_test:#{test_id}")
end
test_result =
if result.success do
%{
@ -684,9 +689,13 @@ defmodule ToweropsWeb.DeviceLive.Form do
Logger.info("Setting snmp_test_result assign: #{inspect(test_result)}")
{:noreply, assign(socket, :snmp_test_result, test_result)}
{:noreply, socket |> assign(:snmp_test_result, test_result) |> assign(:test_id, nil)}
end
# Catch-all for unexpected messages
@impl true
def handle_info(_msg, socket), do: {:noreply, socket}
# Determine effective agent ID considering inheritance chain
defp determine_effective_agent_id(device_params, assigns) do
# Check device-level agent
@ -839,4 +848,14 @@ defmodule ToweropsWeb.DeviceLive.Form do
# 100.64.0.0/10 (RFC6598 CGNAT)
defp non_routable_ipv4_range?(100, b) when b >= 64 and b <= 127, do: true
defp non_routable_ipv4_range?(_a, _b), do: false
@impl true
def terminate(_reason, socket) do
# Unsubscribe from credential test topic if we're still subscribed
if test_id = socket.assigns[:test_id] do
Phoenix.PubSub.unsubscribe(Towerops.PubSub, "credential_test:#{test_id}")
end
:ok
end
end

View file

@ -77,8 +77,8 @@ defmodule ToweropsWeb.DeviceLive.Index do
socket
|> assign(:active_tab, "discovered")
# Use temporary assign - will be cleared after render
|> assign_new(:discovered_devices, fn -> discovered_devices end)
# Use regular assign to prevent state bleeding between tab switches
|> assign(:discovered_devices, discovered_devices)
|> assign(:pagination, %{
page: page,
per_page: per_page,

View file

@ -790,6 +790,10 @@ defmodule ToweropsWeb.GraphLive.Show do
end
end
# Catch-all for unexpected messages
@impl true
def handle_info(_msg, socket), do: {:noreply, socket}
# Perform SNMP polling and update buffer
defp perform_live_poll(socket) do
device_id = socket.assigns.device_id

View file

@ -17,7 +17,7 @@ defmodule ToweropsWeb.MobileQRLive do
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
# Start polling to check if token has been completed
_ =
timer_ref =
if connected?(socket) do
schedule_check()
end
@ -27,6 +27,7 @@ defmodule ToweropsWeb.MobileQRLive do
|> assign(:qr_token, qr_token)
|> assign(:completed, false)
|> assign(:mobile_session, nil)
|> assign(:timer_ref, timer_ref)
{:ok, socket}
end
@ -43,35 +44,57 @@ defmodule ToweropsWeb.MobileQRLive do
user = socket.assigns.current_scope.user
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
timer_ref = schedule_check()
socket =
socket
|> assign(:qr_token, qr_token)
|> assign(:timer_ref, timer_ref)
|> put_flash(:info, t("QR code expired, generated a new one"))
schedule_check()
{:noreply, socket}
else
# Still valid, check again
schedule_check()
{:noreply, socket}
timer_ref = schedule_check()
{:noreply, assign(socket, :timer_ref, timer_ref)}
end
mobile_session ->
# Token was completed!
# Token was completed! Cancel timer since we're done
cancel_timer(socket.assigns.timer_ref)
socket =
socket
|> assign(:completed, true)
|> assign(:mobile_session, mobile_session)
|> assign(:timer_ref, nil)
|> put_flash(:info, t("Mobile device authenticated successfully!"))
{:noreply, socket}
end
end
# Catch-all for unexpected messages
@impl true
def handle_info(_msg, socket), do: {:noreply, socket}
@impl true
def terminate(_reason, socket) do
cancel_timer(socket.assigns[:timer_ref])
:ok
end
defp schedule_check do
Process.send_after(self(), :check_completion, 2000)
end
defp cancel_timer(nil), do: :ok
defp cancel_timer(timer_ref) when is_reference(timer_ref) do
Process.cancel_timer(timer_ref)
:ok
end
# Generate QR code data URL using qrcode.show API (public service)
defp qr_code_url(token) do
# Base URL for the mobile app to handle the token

View file

@ -7,22 +7,29 @@ defmodule ToweropsWeb.Plugs.FilterNoisyLogs do
Currently filters:
- GET /health (Kubernetes health checks)
- GET /health/time (Kubernetes health checks with time sync verification)
- HEAD / (external uptime monitors)
"""
import Plug.Conn
require Logger
def init(opts), do: opts
def call(conn, _opts) do
if should_filter?(conn) do
# Setting phoenix_log to false prevents Phoenix.Logger from logging this request
put_private(conn, :phoenix_log, false)
# Also disable telemetry logging
conn
|> put_private(:phoenix_log, false)
|> put_private(:plug_skip_telemetry, true)
else
conn
end
end
defp should_filter?(%{method: "GET", path_info: ["health"]}), do: true
defp should_filter?(%{method: "GET", path_info: ["health", "time"]}), do: true
defp should_filter?(%{method: "HEAD", path_info: []}), do: true
defp should_filter?(_), do: false
end

View file

@ -0,0 +1,27 @@
defmodule Towerops.Repo.Migrations.FixEquipmentIndexesAfterRename do
use Ecto.Migration
def up do
# The 20260115011502_add_alerts_equipment_indexes migration created indexes
# on the equipment table with equipment-prefixed names.
# The 20260117190134_rename_equipment_to_devices migration renamed the table
# but didn't rename these specific indexes.
# Rename equipment indexes to device indexes
execute "ALTER INDEX IF EXISTS idx_equipment_snmp_poll RENAME TO idx_devices_snmp_poll"
execute "ALTER INDEX IF EXISTS idx_equipment_snmp_monitoring_site RENAME TO idx_devices_snmp_monitoring_site"
# The alerts index references device_id now (column was renamed), but name still says equipment
execute "ALTER INDEX IF EXISTS idx_alerts_equipment_type_unresolved RENAME TO idx_alerts_device_type_unresolved"
end
def down do
# Reverse the renames if rolling back
execute "ALTER INDEX IF EXISTS idx_devices_snmp_poll RENAME TO idx_equipment_snmp_poll"
execute "ALTER INDEX IF EXISTS idx_devices_snmp_monitoring_site RENAME TO idx_equipment_snmp_monitoring_site"
execute "ALTER INDEX IF EXISTS idx_alerts_device_type_unresolved RENAME TO idx_alerts_equipment_type_unresolved"
end
end