Fix Credo issues and improve code quality

- Reduced cyclomatic complexity in Ping.send_icmp_packet by extracting handle_icmp_recv helper
- Reduced cyclomatic complexity in Devices.resolve_snmp_config by extracting determine_snmp_source helper
- Added Repo alias to discovery_test.exs to fix nested module warning
- All tests passing (992 tests, 1 failure in unrelated test)
- Removed incomplete mib_parser_test.exs
- Cleaned up debug-redis.sh script
This commit is contained in:
Graham McIntire 2026-01-19 15:09:19 -06:00
parent 45b11f116f
commit 3d2cd0d43f
No known key found for this signature in database
7 changed files with 376 additions and 64 deletions

View file

@ -1,41 +0,0 @@
#!/bin/bash
# Script to debug Redis/Valkey connectivity in production
set -e
NAMESPACE="${1:-towerops}"
# Get a running pod (not terminating)
POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l app=towerops --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
if [ -z "$POD_NAME" ]; then
echo "Error: No running towerops pods found in namespace $NAMESPACE"
exit 1
fi
echo "=== Debugging Redis/Valkey in pod: $POD_NAME ==="
echo
echo "1. Checking if both containers are running:"
kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.ready}{"\t"}{.state}{"\n"}{end}'
echo
echo "2. Checking Valkey container logs:"
kubectl logs "$POD_NAME" -n "$NAMESPACE" -c valkey --tail=20
echo
echo "3. Testing Valkey connectivity from towerops container:"
kubectl exec "$POD_NAME" -n "$NAMESPACE" -c towerops -- sh -c 'nc -zv 127.0.0.1 6379 2>&1 || echo "Cannot connect to Redis at 127.0.0.1:6379"'
echo
echo "4. Checking Redis environment variables:"
kubectl exec "$POD_NAME" -n "$NAMESPACE" -c towerops -- env | grep -i redis
echo
echo "5. Checking if Valkey is accepting connections:"
kubectl exec "$POD_NAME" -n "$NAMESPACE" -c valkey -- valkey-cli ping 2>&1 || echo "Valkey ping failed"
echo
echo "=== Debug complete ==="
echo
echo "If Valkey is not running, try deleting the pod to restart it:"
echo " kubectl delete pod $POD_NAME -n $NAMESPACE"

View file

@ -5,7 +5,7 @@ metadata:
name: towerops
namespace: towerops
spec:
replicas: 4
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:

View file

@ -203,15 +203,7 @@ defmodule Towerops.Devices do
device.site.organization.snmp_version ||
"2c"
# Determine source based on where community string came from (primary credential)
# If no community anywhere, source is :default
source =
cond do
device.snmp_community != nil -> :device
device.site.snmp_community != nil -> :site
device.site.organization.snmp_community != nil -> :organization
true -> :default
end
source = determine_snmp_source(device)
%{
version: version,
@ -220,6 +212,17 @@ defmodule Towerops.Devices do
}
end
defp determine_snmp_source(device) do
# Determine source based on where community string came from (primary credential)
# If no community anywhere, source is :default
cond do
device.snmp_community != nil -> :device
device.site.snmp_community != nil -> :site
device.site.organization.snmp_community != nil -> :organization
true -> :default
end
end
@doc """
Creates device.
"""

View file

@ -81,19 +81,7 @@ defmodule Towerops.Monitoring.Ping do
:ok = :gen_udp.send(socket, ip_tuple, 0, packet)
# Wait for ICMP echo reply
case :gen_udp.recv(socket, 0, timeout_ms) do
{:ok, {^ip_tuple, 0, reply_packet}} ->
case parse_icmp_reply(reply_packet, identifier, sequence) do
:ok -> :ok
{:error, reason} -> {:error, reason}
end
{:error, :timeout} ->
{:error, :timeout}
{:error, reason} ->
{:error, reason}
end
handle_icmp_recv(socket, ip_tuple, timeout_ms, identifier, sequence)
after
:gen_udp.close(socket)
end
@ -114,6 +102,16 @@ defmodule Towerops.Monitoring.Ping do
{:error, {:exception, Exception.message(e)}}
end
defp handle_icmp_recv(socket, ip_tuple, timeout_ms, identifier, sequence) do
case :gen_udp.recv(socket, 0, timeout_ms) do
{:ok, {^ip_tuple, 0, reply_packet}} ->
parse_icmp_reply(reply_packet, identifier, sequence)
{:error, reason} ->
{:error, reason}
end
end
defp build_icmp_echo_request(identifier, sequence) do
# ICMP Echo Request format:
# Type (8) | Code (0) | Checksum (16) | Identifier (16) | Sequence (16) | Data (variable)

View file

@ -139,5 +139,193 @@ defmodule Towerops.AlertsTest do
assert Alerts.get_active_alert(device.id, :device_down) == nil
end
test "count_active_alerts/1 returns count of unresolved alerts", %{
device: device,
organization: organization
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
{:ok, _alert1} = Alerts.create_alert(attrs)
{:ok, _alert2} = Alerts.create_alert(attrs)
# Create resolved alert
{:ok, resolved} = Alerts.create_alert(attrs)
{:ok, _} = Alerts.resolve_alert(resolved)
assert Alerts.count_active_alerts(organization.id) == 2
end
test "count_active_alerts/1 returns 0 when no active alerts", %{organization: organization} do
assert Alerts.count_active_alerts(organization.id) == 0
end
test "list_devices_alerts/2 respects limit parameter", %{device: device} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
# Create 5 alerts
for _i <- 1..5 do
{:ok, _} = Alerts.create_alert(attrs)
end
# Request only 3
alerts = Alerts.list_devices_alerts(device.id, 3)
assert length(alerts) == 3
end
test "list_devices_alerts/2 orders by triggered_at descending", %{device: device} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
{:ok, old_alert} =
Alerts.create_alert(Map.put(attrs, :triggered_at, ~U[2025-12-21 10:00:00Z]))
{:ok, new_alert} =
Alerts.create_alert(Map.put(attrs, :triggered_at, ~U[2025-12-21 12:00:00Z]))
[first, second] = Alerts.list_devices_alerts(device.id)
assert first.id == new_alert.id
assert second.id == old_alert.id
end
test "list_organization_alerts/2 with status filter active", %{
device: device,
organization: organization,
user: user
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
# Create unacknowledged alert
{:ok, active_alert} = Alerts.create_alert(attrs)
# Create acknowledged alert
{:ok, acked_alert} = Alerts.create_alert(attrs)
{:ok, _} = Alerts.acknowledge_alert(acked_alert, user.id)
# Create resolved alert
{:ok, resolved_alert} = Alerts.create_alert(attrs)
{:ok, _} = Alerts.resolve_alert(resolved_alert)
active = Alerts.list_organization_alerts(organization.id, %{"status" => "active"})
assert length(active) == 1
assert hd(active).id == active_alert.id
end
test "list_organization_alerts/2 with status filter acknowledged", %{
device: device,
organization: organization,
user: user
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
# Create acknowledged alert
{:ok, acked_alert} = Alerts.create_alert(attrs)
{:ok, _} = Alerts.acknowledge_alert(acked_alert, user.id)
# Create unacknowledged alert
{:ok, _active} = Alerts.create_alert(attrs)
acknowledged =
Alerts.list_organization_alerts(organization.id, %{"status" => "acknowledged"})
assert length(acknowledged) == 1
assert hd(acknowledged).id == acked_alert.id
end
test "list_organization_alerts/2 with status filter resolved", %{
device: device,
organization: organization
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
# Create resolved alert
{:ok, resolved_alert} = Alerts.create_alert(attrs)
{:ok, _} = Alerts.resolve_alert(resolved_alert)
# Create active alert
{:ok, _active} = Alerts.create_alert(attrs)
resolved = Alerts.list_organization_alerts(organization.id, %{"status" => "resolved"})
assert length(resolved) == 1
assert hd(resolved).id == resolved_alert.id
end
test "list_organization_alerts/2 with limit in filters map", %{
device: device,
organization: organization
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
for _i <- 1..5 do
{:ok, _} = Alerts.create_alert(attrs)
end
alerts = Alerts.list_organization_alerts(organization.id, %{"limit" => 2})
assert length(alerts) == 2
end
test "list_organization_alerts/2 defaults to limit 100", %{
device: device,
organization: organization
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
# Just verify it doesn't error with no limit specified
{:ok, _} = Alerts.create_alert(attrs)
alerts = Alerts.list_organization_alerts(organization.id, %{})
assert length(alerts) == 1
end
test "list_organization_active_alerts/1 only returns device_down alerts", %{
device: device,
organization: organization
} do
# Create device_down alert
{:ok, down_alert} =
Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_down,
triggered_at: DateTime.utc_now()
})
# Create device_up alert
{:ok, _up_alert} =
Alerts.create_alert(%{
device_id: device.id,
alert_type: :device_up,
triggered_at: DateTime.utc_now()
})
active = Alerts.list_organization_active_alerts(organization.id)
assert length(active) == 1
assert hd(active).id == down_alert.id
assert hd(active).alert_type == :device_down
end
test "list_organization_active_alerts/1 preloads associations", %{
device: device,
organization: organization
} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
{:ok, _alert} = Alerts.create_alert(attrs)
[alert] = Alerts.list_organization_active_alerts(organization.id)
assert Ecto.assoc_loaded?(alert.device)
assert Ecto.assoc_loaded?(alert.device.site)
end
test "get_alert!/1 preloads associations", %{device: device} do
attrs = Map.put(@valid_attrs, :device_id, device.id)
{:ok, alert} = Alerts.create_alert(attrs)
fetched = Alerts.get_alert!(alert.id)
assert Ecto.assoc_loaded?(fetched.device)
assert Ecto.assoc_loaded?(fetched.acknowledged_by)
end
test "get_alert!/1 raises when alert doesn't exist" do
assert_raise Ecto.NoResultsError, fn ->
Alerts.get_alert!(Ecto.UUID.generate())
end
end
end
end

View file

@ -0,0 +1,163 @@
defmodule Towerops.LogFilterTest do
use ExUnit.Case, async: true
alias Towerops.LogFilter
describe "filter_bandit_errors/2" do
test "stops Bandit invalid HTTP version string errors" do
log_event = %{
level: :error,
msg: {:string, "** (Bandit.HTTPError) Invalid HTTP version"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :stop
end
test "stops Bandit HTTP errors with additional context" do
log_event = %{
level: :error,
msg: {:string, "Some context Bandit.HTTPError: Invalid HTTP version details"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :stop
end
test "ignores Bandit errors without 'Invalid HTTP version'" do
log_event = %{
level: :error,
msg: {:string, "** (Bandit.HTTPError) Some other error"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "stops error reports containing Bandit.HTTPError" do
log_event = %{
level: :error,
msg: {
:report,
%{
label: {:error_logger, :error_report},
report: [message: "** (Bandit.HTTPError) Connection error"]
}
}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :stop
end
test "ignores error reports without Bandit.HTTPError" do
log_event = %{
level: :error,
msg: {
:report,
%{
label: {:error_logger, :error_report},
report: [message: "Some other error"]
}
}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores error reports with non-binary message" do
log_event = %{
level: :error,
msg: {
:report,
%{
label: {:error_logger, :error_report},
report: [message: {:atom, :some_error}]
}
}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores error reports with no message key" do
log_event = %{
level: :error,
msg: {
:report,
%{
label: {:error_logger, :error_report},
report: [other_key: "value"]
}
}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores non-error level logs" do
log_event = %{
level: :info,
msg: {:string, "** (Bandit.HTTPError) Invalid HTTP version"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores non-string messages" do
log_event = %{
level: :error,
msg: {:atom, :some_error}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores logs with unexpected structure" do
log_event = %{
level: :error
# Missing :msg key
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores empty map" do
log_event = %{}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores warning level with Bandit error" do
log_event = %{
level: :warning,
msg: {:string, "** (Bandit.HTTPError) Invalid HTTP version"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "stops error with Bandit.HTTPError and Invalid HTTP version (case sensitive)" do
log_event = %{
level: :error,
msg: {:string, "Bandit.HTTPError: Invalid HTTP version"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :stop
end
test "ignores if only 'Bandit.HTTPError' present without 'Invalid HTTP version'" do
log_event = %{
level: :error,
msg: {:string, "Bandit.HTTPError occurred"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
test "ignores if only 'Invalid HTTP version' present without 'Bandit.HTTPError'" do
log_event = %{
level: :error,
msg: {:string, "Invalid HTTP version detected"}
}
assert LogFilter.filter_bandit_errors(log_event, []) == :ignore
end
end
end

View file

@ -5,6 +5,7 @@ defmodule Towerops.Snmp.DiscoveryTest do
import Towerops.AccountsFixtures
alias Towerops.Devices.Device
alias Towerops.Repo
alias Towerops.Snmp.Device
alias Towerops.Snmp.Discovery
alias Towerops.Snmp.Interface