feat(rmm): device-sync worker, provider trait, suppression + dedupe (PMS-103) #62
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/pms-103-rmm-sync-worker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes the five PMS-100 blockers that the story-verification pass surfaced - device-sync worker, RmmProvider trait, real encryption key, suppression + dedupe + TicketService routing on alert ingest, missing GET/PUT connection endpoints - in one PR so the worker ships with the integration it actually needs. Mirrors the PMS-92 shape used for the notifications dispatcher.
RmmService::new(db)is gone; the only constructors arewith_encryption_key(db, key)andwith_dependencies(db, key, ticket_service).src/api/router.rs:110now wires the realencryption_key(same value already threaded intoBillingService+NotificationsService) plus aTicketServiceclone, sormm_connections.api_key_encrypted/api_secret_encryptedstop being encrypted under[0u8; 32]and alert-ingest tickets stop bypassing the canonical ticket-creation path.RmmProvidertrait lives insrc/modules/rmm/provider.rswithlist_devices/list_alerts/ping.TacticalRmmProvideris the first real implementation (Token auth against/agents/and/alerts/).mesh_central/datto/connectwise/ninja_rmmfall back to a fail-loudUnimplementedProviderso an operator using one of those sees a reallast_errorinstead of silent no-ops.migrations/007_rmm_mesh_central_provider.sqladdsmesh_centralto thermm_connections.providerCHECK constraint so PMS-105's "MeshCentral provider variant" claim matches the schema.RmmSyncWorker(src/modules/rmm/worker.rs) spawned fromsrc/main.rsnext to the notificationsDispatcherWorker. Tick is 60s; per active connection past itssync_interval_minuteswindow the worker decrypts credentials, builds the right provider, pulls devices, UPSERTsrmm_device_mappingson(rmm_connection_id, rmm_device_id), match-or-createsassets(serial -> hostname order), stampslast_sync_at, writesasset_audit_logwithaction='created'or'synced', and flipssync_statustosuccess(clearinglast_error) orfailed(storing the error). Errors per connection are isolated so one bad provider does not stall the rest.ingest_alertatsrc/modules/rmm/service.rs:296-393consultsrule.suppression_rules.min_severityandrule.suppression_rules.dedupe_window_minutesbefore creating a ticket.ticket_templateJSONB is rendered via the notificationsrender_templatehelper. Ticket creation goes throughTicketService::create_ticketso the result lands with validation, automation, audit log, and the notifications dispatch wire-up shipped in PMS-92.GET /api/v1/rmm/connections/{id}andPUT /api/v1/rmm/connections/{id}land. PUT usesCOALESCEso missing fields leave the existing column untouched;api_key/api_secretonly re-encrypt when the payload supplies them.tests/rmm.rscovers the full loop:.forgejo/workflows/check.ymladds--test rmmto the integration-tests step.Test plan
--test rmm.just dev) and confirm bothnotifications dispatcher worker startedandrmm sync worker startedlog at startup.rmm_connectionsrow pointing at a real Tactical RMM (or a stub); confirm the worker flipssync_statustosuccessorfailedwithin ~60s and stampslast_sync_at./api/v1/rmm/alertswith a valid HMAC signature against a rule that hassuppression_rules.min_severityset; confirm a low-severity alert produces no ticket and a critical one produces exactly one ticket throughTicketService(visible intickets.source = 'rmm'with a sequencedticket_number)./api/v1/rmm/connections/{id}with onlyis_active = false; confirmapi_key_encrypteddoes NOT change in the database (PUTpreserves credentials when omitted).#PMS-103
Closes the five PMS-100 blockers that the story-verification pass surfaced, in a single PR so the device-sync worker ships with the integration it needs to drain real traffic. Mirrors the PMS-92 shape on notifications. - Drop RmmService::new(); only with_encryption_key / with_dependencies survive. Router constructed RmmService with [0u8; 32], so rmm_connections.api_key_encrypted / api_secret_encrypted were ciphertext under a zero key (effectively plaintext). Router now threads the real encryption_key (same value already routed to BillingService + NotificationsService) plus a TicketService clone so alert ingest can use the canonical ticket creation path. - RmmProvider trait (src/modules/rmm/provider.rs) abstracts the per-platform HTTP dialect. TacticalRmmProvider implements list_devices / list_alerts / ping against Tactical RMM's /agents/ and /alerts/ endpoints with Token auth. build_provider() returns a fail-loud UnimplementedProvider for mesh_central / datto / connectwise / ninja_rmm so operators see a clear last_error instead of silent no-ops. - Migration 007_rmm_mesh_central_provider.sql adds mesh_central to the rmm_connections.provider CHECK constraint so PMS-105 (variant marked resolved) actually matches reality. - RmmSyncWorker (src/modules/rmm/worker.rs) spawned from main.rs. Per-tick (60s) it picks every active connection past its sync_interval_minutes window, decrypts credentials, builds the right provider, list_devices() and UPSERTs rmm_device_mappings on (rmm_connection_id, rmm_device_id), match-or-creates assets via serial -> hostname order, stamps last_sync_at, writes asset_audit_log with action='created' or 'synced', and flips the connection's sync_status to 'success' (with last_error cleared) or 'failed' (with the error stored). Errors per connection are isolated so one bad provider does not stall the rest. sync_one(...) is public so the integration test can drive a deterministic tick. - ingest_alert at src/modules/rmm/service.rs now consults rule.suppression_rules.min_severity and rule.suppression_rules.dedupe_window_minutes before creating a ticket. Suppressed / deduped alerts are dropped after a debug log. ticket_template JSONB is rendered via the notifications module's render_template helper (exported as crate::modules::notifications::render_template). Ticket creation routes through TicketService::create_ticket so the result lands with validation, automation, audit log, and the notifications dispatch wire-up shipped in PMS-92. Legacy direct-INSERT path kept under a tickets = None fallback for old test fixtures. - GET /api/v1/rmm/connections/{id} + PUT /api/v1/rmm/connections/{id} land. PUT uses COALESCE so missing fields leave the existing column untouched; api_key / api_secret only re-encrypt when explicitly supplied. - tests/rmm.rs covers the full loop: one worker tick against a static mock provider asserts mapping, asset creation with serial + last_sync_at + rmm_device_id, asset_audit_log row, and connection sync_status='success'. Alert ingest tests assert ticket landed via TicketService (T-prefixed ticket_number from the sequence, template-rendered title + description), suppression below min_severity drops the alert, and a second alert inside the dedupe window does not open a new ticket. Encryption guard: POST a canary in api_key and assert the persisted api_key_encrypted bytes do NOT contain it verbatim. - CI: .forgejo/workflows/check.yml adds --test rmm to the integration-tests step. #PMS-103