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:
parent
45b11f116f
commit
3d2cd0d43f
7 changed files with 376 additions and 64 deletions
|
|
@ -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"
|
||||
|
|
@ -5,7 +5,7 @@ metadata:
|
|||
name: towerops
|
||||
namespace: towerops
|
||||
spec:
|
||||
replicas: 4
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
163
test/towerops/log_filter_test.exs
Normal file
163
test/towerops/log_filter_test.exs
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue