Wrapped all Exq.Api calls in individual try/rescue/catch blocks to handle
GenServer exits and exceptions when Exq hasn't connected to Redis yet.
The "GenServer Exq terminating" errors in logs are from Exq itself crashing
when it receives calls before it's ready, not from our code. These will still
appear in logs as Exq restarts, but our telemetry code won't crash.
Changes:
- Added try/rescue/catch around each queue_size call in the loop
- Added try/rescue/catch around processes/busy calls
- Kept outer try/rescue/catch as a final safety net
- rescue blocks must come before catch blocks in Elixir
Fixed two profile import issues:
1. Skip sensors without OIDs:
- Some profiles have sensors with null/missing OID fields
- These can't be used for SNMP polling anyway
- Added check to skip importing sensors without oid or num_oid
- Prevents Ecto.Changeset validation errors
2. Truncate MIB values to 255 characters:
- ERROR 22001 (string_data_right_truncation)
- MIB field is varchar(255) but some values exceed this
- Added truncation to first 255 chars before saving
- Location: lib/towerops/device_profiles/importer.ex:229-236
Both errors were causing profile imports to fail with crashes.
Changed pattern matching to proper error handling in telemetry functions
to prevent crashes when Exq or Redis connections aren't available.
Issues fixed:
1. Exq.Api.queue_size/2 calls were using pattern matching {:ok, size} =
which crashed with FunctionClauseError when Exq wasn't connected to Redis
2. Redix.start_link/1 and Redix.command/2 calls were using pattern matching
which crashed when Redis wasn't available
Changes:
- publish_exq_stats: Use case statements for queue_size calls
- publish_exq_stats: Use with statement for processes/busy calls
- publish_redis_stats: Use with statement for all Redis operations
This allows the app to start and run even when Redis/Exq connections
aren't immediately available, which is especially important during
container startup and rolling deployments.
Fixed two import-related errors:
1. DateTime microseconds error when updating API token last_used_at:
- Error: :utc_datetime expects microseconds to be empty
- Fix: Use DateTime.truncate(:second) to remove microseconds
- Location: lib/towerops/api_tokens.ex:179
2. Float parsing error for integer strings like "0":
- Error: :erlang.binary_to_float("0") - not a textual representation of float
- Fix: Use Float.parse/1 instead of String.to_float/1
- Float.parse/1 handles both "0" and "0.0" correctly
- Returns {float, remainder} on success, :error on failure
- Location: lib/towerops/device_profiles/importer.ex:380
Both errors were causing profile imports to fail with crashes.
The `not` operator in Elixir only works with booleans, not nil values.
When Exq process doesn't exist, Process.whereis(Exq) returns nil, causing
`:badarg` error when used with `not`.
Error:
Class=:error
Reason=:badarg
{:erlang, :not, [nil], [error_info: %{module: :erl_erts_errors}]}
Changed from:
not Process.whereis(Exq)
To:
is_nil(Process.whereis(Exq))
This properly checks if the process exists without causing runtime errors.
The token success modal was missing z-index classes that caused the modal
content to appear behind the grey backdrop. Users could only see the
checkmark icon and couldn't interact with the modal properly.
Changes:
- Added z-0 class to backdrop div
- Added relative z-10 classes to span and modal content div
- Added unique ID to modal (token-success-modal)
- Added unique ID to modal title (token-success-modal-title)
This matches the z-index structure used in the "add token" modal.
API token creation fix:
- Handle both nested (%{"token" => %{...}}) and flat params
- Support URL-encoded form data from LiveView
- Add validation for required name and organization_id fields
Rust agent raw ICMP ping implementation:
- New ping module with async ICMP echo request/reply
- Build ICMP packets manually with proper checksum calculation
- Use socket2 for raw socket creation (requires CAP_NET_RAW)
- Parse both raw ICMP and IP-wrapped responses
- Include unit tests for checksum and packet building
- Dependencies: socket2 v0.5, rand v0.8
- Compiles successfully with release profile optimizations
AutoDismissFlash hook fix:
- Move hook definitions before LiveSocket initialization
- Ensures hooks are defined when referenced in LiveSocket constructor
- Remove duplicate Hooks object
Raw ICMP ping implementation:
- Replace system ping command with pure Elixir implementation
- Use raw ICMP sockets for echo request/reply
- Build ICMP packets manually with proper checksum calculation
- Parse IP addresses using :inet.parse_address/1
- Handle both raw ICMP and IP-wrapped responses
- Requires CAP_NET_RAW capability in production containers
- Suitable for containerized environments without ping command
User deletion bug:
- Store client IP in socket assigns during mount
- Access stored IP during delete event instead of connect_info
- Fixes RuntimeError when attempting to delete users
Timezone support:
- Add timezone offset maps for common timezones (no external deps)
- Implement shift_timezone/2 helper using DateTime.add/3
- Add timezone-aware format_datetime/2, format_date/2, format_iso8601/2
- Keep backwards-compatible 1-arity versions defaulting to UTC
- Add comprehensive test coverage for timezone conversions
- All tests passing (23 tests, 0 failures)
Note: Timezone offsets are standard time only (no DST handling)
Common timezones supported: America/New_York, America/Los_Angeles,
Europe/London, Europe/Paris, Asia/Tokyo, Asia/Shanghai, Australia/Sydney
- Replace JavaScript modal handlers with LiveView event bindings
- Add phx-click events for show_add_token_modal and cancel_add_token
- Convert plain HTML form to LiveView form with phx-submit
- Remove outdated window.liveSocket.execJS JavaScript code
- Add timezone parameter support to TimeHelpers functions:
- format_datetime/2 now accepts timezone and shifts DateTime
- format_date/2 now accepts timezone and shifts DateTime
- format_iso8601/2 now accepts timezone and shifts DateTime
- All helper functions fallback to UTC if timezone is invalid or nil
- Replace TODO comment with "Future enhancement" note
- Alias nested DeviceProfiles.Importer module
- Replace expensive length/1 check with empty list comparison
- Extract nested logic into helper functions to reduce complexity:
- import_sensor_with_states for sensor import
- walk_sensor_oid and walk_processor_oid for SNMP walks
- apply_processor_precision and determine_processor_status
- check_device_health for device monitoring
- Add auto-dismiss functionality to flash messages (30 second timeout)
- Implement AutoDismissFlash LiveView hook in TypeScript
- Add tab navigation UI with Personal, Account, Security, API, Notifications tabs
- Add active_tab state management in LiveView
- Add switch_tab event handler
- Show only active tab content (like Tailwind Plus template)
- Remove section prefixes since each is now on separate tab
- Default to Personal tab on mount
Prevents duplicate polling in multi-node production environment:
- Replace DynamicSupervisor with Horde.DynamicSupervisor (distributed)
- Replace Registry with Horde.Registry (cluster-aware)
- Each device monitor runs on exactly ONE node across the cluster
- Each SNMP poller runs on exactly ONE node across the cluster
- Automatic failover when nodes go down
- Automatic rebalancing when nodes join/leave
Architecture:
- Horde.Registry for process naming (distributed lookup)
- Horde.DynamicSupervisor for DeviceMonitor processes
- Horde.DynamicSupervisor for PollerWorker processes
- Task.Supervisor remains local per-node for parallel operations
- members: :auto enables automatic cluster discovery via libcluster
Production impact:
- 2 replicas = devices split ~50/50 across both pods
- No duplicate SNMP polls to same device
- If pod dies, surviving pod takes over all devices
- When new pod starts, devices rebalance automatically
Dependencies added:
- horde ~> 0.9.0 (distributed supervisor/registry)
- delta_crdt (Horde dependency for CRDT-based state)
- libring (Horde dependency for consistent hashing)
- Use SNMP poll success as health indicator for SNMP-enabled devices
- Fall back to ICMP ping only for non-SNMP devices or stale SNMP data
- Add connection retry logic to Exq with backoff
- Add better error logging for ping permission issues
- Resolves issue where devices showed as down despite successful SNMP polling
Root cause: Container runs as non-root (UID 65534) without ICMP permissions,
so ping fails even though SNMP (UDP) works. Now using SNMP poll timestamps
to determine device status when SNMP is enabled.
- Check if Exq process is running before attempting to collect metrics
- Change rescue to catch to handle process exits (:noproc)
- Prevents error logs in environments where Exq is not configured
- Update user settings page to use Tailwind Plus 3-column grid layout
- Add API token management section to user settings
- Implement organization name display in navigation menu
- Update table component to use Tailwind Plus styling across all tables
- Update flash notifications with Tailwind Plus design
- Update dropdown menus and select inputs with custom chevron styling
- Fix authentication redirect to return to original page after login
- Preserve user_return_to session key across session renewal
- Add store_return_to_for_liveview plug to browser pipeline
- Add profiles.json to gitignore
Changes profile import endpoint to use standard API token authentication:
API Token Changes:
- Add user_id to api_tokens table (tracks who created the token)
- Update ApiTokens.verify_token/1 to return user along with org_id
- Update ApiAuth plug to assign current_user from token
Profile Import Changes:
- Move endpoint from /api/v1/admin/profiles/import to /api/v1/profiles/import
- Check user.is_superuser in controller instead of using RequireSuperuser plug
- Use api_v1 pipeline (Bearer token auth) instead of browser session
- Update documentation to show API token usage
Security:
- Only API tokens created by superusers can import profiles
- Returns 403 Forbidden if token user is not a superuser
- Logs import attempts with user email for audit trail
This provides a consistent API experience using Bearer tokens
instead of requiring browser session cookies.
Add export and import functionality for device SNMP profiles:
Export (local):
- New mix task: mix export_profiles --librenms-path ~/dev/librenms --output profiles.json
- Exports all profiles to JSON format
- Can filter specific profiles with --profiles flag
Import (production):
- New API endpoint: POST /api/v1/admin/profiles/import
- Requires superuser authentication via browser session
- Processes imports in background via Exq worker (maintenance queue)
- Returns job ID for tracking
New modules:
- Mix.Tasks.ExportProfiles - Export profiles to JSON
- ProfileImportWorker - Background job for importing profiles
- ProfilesController - API endpoint for bulk import
- RequireSuperuser plug - Restricts access to superusers
- Importer.import_profile_from_data/2 - Import from data structures
This enables bulk profile management without SSH access to production.
- Fix divisor calculation in importer: use precision value directly instead of 10^precision
- Fix Dynamic profile to handle map format from Client.walk (was expecting list)
- Add processor discovery to Dynamic profile (converts CPU to percent sensors)
- Fix sensor index extraction to clean template variables
- Make sensor indices unique using descriptive names for scalar sensors
- Add status determination for CPU sensors (ok/warning/critical thresholds)
This enables proper discovery and monitoring of ePMP devices with:
- DFS count sensors
- GPS/DFS/antenna state sensors
- CPU usage monitoring
All sensors now discover correctly without unique constraint violations.
Fixed three compile warnings by removing unreachable error handling:
1. Dynamic.discover_sensors/2 always returns {:ok, sensors}, never errors
- Removed unreachable error clause in discovery.ex
- Updated spec to reflect actual return type
2. parse_detection_rule/2 always returns {:ok, :processed}
- Simplified create_detection_rules/2 to use Enum.each instead of reduce_while
- Removed unreachable error handling in caller
3. import_all_from_directory/2 always returns {:ok, %{...}}
- Removed unreachable error clause in Mix task
- Simplified to direct pattern match
All code now compiles without warnings.
Replace all references with generic terms:
- 'external YAML files' instead of specific project names
- 'device profiles' for database-stored definitions
- 'imported OID definitions' for sensor configurations
This makes the codebase vendor-neutral and focused on the
internal implementation rather than external dependencies.
LibreNMS sensor OIDs contain template variables like '.{{ $index }}'
which need to be removed before performing SNMP walks.
Added clean_oid_template/1 to strip these template variables:
- '.1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}' -> '.1.3.6.1.4.1.17713.21.2.1.36'
- 'sysUptime.{{ $index }}' -> 'sysUptime'
The SNMP walk will now use the base OID and discover all instances
with their actual index values.
LibreNMS OS definitions use MIB symbolic names (e.g.,
CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0) which require
MIB compilation to resolve to numeric OIDs.
The SNMP client (snmpkit) only supports numeric OIDs, not MIB names.
Disabled OS definition polling for now. The system will use the profile
OS name as firmware_version (e.g., 'epmp') until MIB resolution is
implemented.
Future improvements:
- Add MIB compiler integration (net-snmp, snmptranslate)
- Import numeric OIDs during profile import
- Add MIB -> numeric OID lookup table
Enhance dynamic profile to poll OS definitions from SNMP:
- Add serial_number field to snmp_devices table
- Update Dynamic profile to query profile_os_definitions
- Poll SNMP OIDs for version and serial fields
- Display actual firmware version instead of profile name
For ePMP devices, this will now query:
- CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0 for firmware
- CAMBIUM-PMP80211-MIB::cambiumEPMPMSN.0 for serial number
The Operating System field will now show actual version like
'Cambium 4.7.1' instead of 'Cambium epmp'.
The Operating System field was showing 'Cambium Verona Networks' because
firmware_version was set to the raw sys_descr SNMP value.
Now uses the profile's OS name (e.g., 'epmp') so it displays as
'Cambium epmp' which is cleaner and more accurate.
Future improvement: Poll actual firmware version from SNMP using the
profile_os_definitions.version OID.
When clicking 'Rediscover Device', navigate to the device details page
where the user can see live updates as discovery progresses.
The device show page already subscribes to PubSub discovery events and
will automatically refresh when discovery completes, showing updated
manufacturer, model, sensors, and interfaces.
LibreNMS uses leading dot format (.1.3.6.1...) but SNMP returns
OIDs without leading dot (1.3.6.1...). Normalize by prepending
dot to sys_object_id before matching, and use starts_with instead
of contains for more accurate prefix matching.
- Configure Phoenix.PubSub to use Redis adapter for pod-resilient messaging
- Add PubSub broadcasts for sensor readings updates in PollerWorker
- Add PubSub broadcasts for interface stats updates in PollerWorker
- Add PubSub broadcasts for neighbor discovery updates in PollerWorker
- Add PubSub broadcasts for monitoring check updates in MonitorWorker
- Broadcast interface and sensor events to device-specific topics
- Add event handlers in DeviceLive.Show for all update types
- Device show page now updates in real-time without polling
- Updates survive pod restarts and work across multiple pods
Added telemetry metrics for monitoring:
Exq Metrics:
- Queue sizes for all queues (default, discovery, polling, monitoring, maintenance)
- Number of busy worker processes
- Total number of worker processes
Redis/Valkey Metrics:
- Connected clients
- Memory usage
- Total commands processed
Metrics are collected every 10 seconds via telemetry_poller and displayed
in LiveDashboard at /dashboard
Updated all references to use the more technically accurate term 'ICMP Latency'
instead of 'Ping Latency' in:
- Graph titles and labels
- HTML comments
- Test assertions
- Added comprehensive tests for GraphLive.Show to ensure:
* Page renders with correct assigns
* Uses @current_organization (not @organization)
* Latency, processor, memory, and traffic graphs work
* Time range selection works correctly
* All required assigns are present
- Reordered device show page so Overall Traffic appears above Ping Latency
Changed device_monitor.ex to always use ICMP ping for monitoring checks,
regardless of whether SNMP is enabled for the device.
Rationale:
- ICMP ping gives actual network latency (round-trip time)
- SNMP checks are for data collection (interfaces, sensors, etc.)
- Both should run independently for SNMP-enabled devices:
* SNMP polling: Collects device metrics (handled by poller_worker)
* ICMP monitoring: Measures latency and availability (handled by device_monitor)
This ensures latency graphs always show actual network response time,
not SNMP query response time.
The monitoring system was creating checks but hardcoding response_time_ms
to nil instead of using the actual ping latency. This caused the latency
graphs to have no data to display.
Changes:
- device_monitor.ex: Capture response time from ping/SNMP check result
- monitor_worker.ex: Use correct field name (response_time_ms) and status enum values (:success/:failure)
After this fix, new monitoring checks will include latency data and the
latency graphs will display properly on both device and site detail pages.
Shows ping latency for all devices in the site over the last 24 hours.
Each device is displayed as a separate line on the graph with its name
as the label.
Graph only appears if there is latency data available for at least
one device in the site.
Created API token system for programmatic access:
- API tokens table with organization scoping
- ApiTokens context for token management
- ApiAuth plug for Bearer token authentication
- Tokens shown once on creation, then hashed for security
Implemented RESTful API v1 endpoints:
- SitesController: CRUD operations for sites
- DevicesController: CRUD operations for devices
- All operations scoped to authenticated organization
- Proper authorization checks via site ownership
Technical details:
- Tokens prefixed with "towerops_" for easy identification
- SHA-256 hashing for token storage
- Last used timestamp tracking (async update)
- Optional token expiration support
- Standard JSON error responses (40x status codes)
Routes:
- /api/v1/sites (GET, POST, PATCH, DELETE)
- /api/v1/devices (GET, POST, PATCH, DELETE)
Authentication:
- Authorization: Bearer towerops_xxxxx header required
- Returns 401 for invalid/expired tokens
- Returns 403 for unauthorized resource access
Created three new Exq workers to handle background jobs:
- PollWorker: SNMP polling operations (queue: polling)
- MonitorWorker: Device monitoring checks (queue: monitoring)
- DiscoveryWorker: SNMP discovery (already existed)
Updated files to use Exq.enqueue instead of Task.start:
- lib/towerops/snmp/poller_worker.ex
- lib/towerops/monitoring/device_monitor.ex
- lib/towerops_web/channels/agent_channel.ex
Each module includes enqueue_* helper functions that:
- Use Task.start in test environment for synchronous execution
- Use Exq.enqueue in dev/prod for proper background job processing
Added "monitoring" queue to Exq configuration in application.ex
All tests passing (824 tests, 0 failures)
- Create DiscoveryWorker for handling SNMP device discovery
- Replace Task.start with Exq.enqueue for discovery operations
- Enqueue jobs to 'discovery' queue with device_id parameter
- Add enqueue_discovery/1 helper that falls back to Task.start in test
- Discovery jobs now persistent and retryable via Exq
- Triggered on: new device creation, device update, manual rediscover button