From d1b01e0fb0f154e4e1c2bde3b5c57062f1ad2a7e Mon Sep 17 00:00:00 2001 From: gnacho Date: Wed, 21 Oct 2026 07:48:44 +0200 Subject: [PATCH] feat(modules): handle the config_backup command from NetPulse (#553) Create a fresh snapshot (CreateSnapshot) and push it through the existing PushLatestSnapshot path when the embedded agent receives the config_backup SSE event. Best-effort: failures go to the log. Closes #553 --- go.mod | 2 +- go.sum | 6 +-- internal/modules/netpulse_agent.go | 42 +++++++++++++------- internal/modules/snapshot_push.go | 17 ++++++++ internal/modules/snapshot_push_553_test.go | 45 ++++++++++++++++++++++ 5 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 internal/modules/snapshot_push_553_test.go diff --git a/go.mod b/go.mod index 844831b1..4a0122ba 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gnacho/netgrip go 1.26.0 require ( - github.com/gnacho/netpulse/agent v1.5.5 + github.com/gnacho/netpulse/agent v1.5.6 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 5e88b1cd..7c05821f 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/gnacho/netpulse/agent v1.5.4 h1:dxQwJY5z81DlUyZUgDlx21TRzaJc8j3CHNB2u/KYS6w= -github.com/gnacho/netpulse/agent v1.5.4/go.mod h1:TEBH43X4N9t/UbxQzTg0WTi1V1eKZYU2zc0v4REbg90= -github.com/gnacho/netpulse/agent v1.5.5 h1:hpRWCFED4dVAjyD3myPcebslNMB/87WiS4jBHh5s/4M= -github.com/gnacho/netpulse/agent v1.5.5/go.mod h1:TEBH43X4N9t/UbxQzTg0WTi1V1eKZYU2zc0v4REbg90= +github.com/gnacho/netpulse/agent v1.5.6 h1:ANjkn+EDgYCihw1XBcyqm+Lg0tST1UEtBfXnte2IKTE= +github.com/gnacho/netpulse/agent v1.5.6/go.mod h1:TEBH43X4N9t/UbxQzTg0WTi1V1eKZYU2zc0v4REbg90= github.com/gonzalop/mq v0.9.11 h1:cKdoCT3dgiz3kV53QKUDqtcdDHCpMHS8nXrxVhqt3gE= github.com/gonzalop/mq v0.9.11/go.mod h1:QVPtaf408IuhzPJO4w2Q0sjzM+nP6FWHyfyXUjvY/PQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/internal/modules/netpulse_agent.go b/internal/modules/netpulse_agent.go index e4ae3216..e093591a 100644 --- a/internal/modules/netpulse_agent.go +++ b/internal/modules/netpulse_agent.go @@ -503,19 +503,20 @@ func applyNetPulseAgent(p netpulsePaths) { } opts := runtime.Options{ - Server: cfg.Server, - Token: cfg.Token, - Slug: cfg.Slug, - ServerFP: cfg.ServerFP, - Interval: parseNetPulseInterval(cfg.Interval), - ScanInterval: parseNetPulseScanInterval(cfg.ScanInterval), - WanTarget: cfg.WanTarget, - GwTarget: cfg.GwTarget, - EnvFile: p.env, - Version: version, - Kind: "netgrip", - SelfMQTT: netPulseSelfMQTT, - OnUpgrade: netPulseUpgradeTrigger, + Server: cfg.Server, + Token: cfg.Token, + Slug: cfg.Slug, + ServerFP: cfg.ServerFP, + Interval: parseNetPulseInterval(cfg.Interval), + ScanInterval: parseNetPulseScanInterval(cfg.ScanInterval), + WanTarget: cfg.WanTarget, + GwTarget: cfg.GwTarget, + EnvFile: p.env, + Version: version, + Kind: "netgrip", + SelfMQTT: netPulseSelfMQTT, + OnUpgrade: netPulseUpgradeTrigger, + OnConfigBackup: netPulseConfigBackupTrigger, } ctx, cancel := context.WithCancel(base) @@ -618,6 +619,21 @@ func statusCallbackGen(gen int) func(runtime.Status) { } } +// netPulseConfigBackupTrigger: evento SSE "config_backup" del servidor +// NetPulse (netpulse#1462 / netgrip#553): snapshot standalone de la config, +// independiente del flujo de firmware. Crea un snapshot fresco con el +// módulo propio y lo empuja al endpoint genérico de backups. Best-effort: +// los fallos van a log; NetPulse descubre el resultado al recibir (o no) la +// subida. +func netPulseConfigBackupTrigger(_ string) { + res := CreateAndPushSnapshot() + if !res.Ok { + log.Printf("netpulse: config_backup solicitado por el servidor falló: %s", res.Error) + return + } + log.Printf("netpulse: config_backup solicitado por el servidor → snapshot %s subido", res.SnapshotID) +} + // netPulseUpgradeTrigger: evento SSE "upgrade" del servidor NetPulse (#363): // NetGrip se actualiza a sí mismo con su propio updater (sus releases de // GitHub, sha verificado); el servidor solo dispara. Si ya está en la última, diff --git a/internal/modules/snapshot_push.go b/internal/modules/snapshot_push.go index d6a844d6..29ce2bf8 100644 --- a/internal/modules/snapshot_push.go +++ b/internal/modules/snapshot_push.go @@ -47,6 +47,23 @@ func SetPushConfig(cfg PushConfig) error { return os.WriteFile(pushConfigPath, data, 0600) } +// CreateAndPushSnapshot (#553): crea un snapshot fresco y lo empuja a +// NetPulse por el camino existente. Es la respuesta al evento SSE +// "config_backup" de netpulse#1462: un backup standalone, independiente del +// flujo de upgrade. Devuelve el resultado del push (o del propio create). +func CreateAndPushSnapshot() PushResult { + if _, err := createSnapshotFn(); err != nil { + return PushResult{Error: fmt.Sprintf("create snapshot: %s", err)} + } + return pushLatestFn() +} + +// Inyectables en tests (mismo patrón que netPulseMQTTEnvPath). +var ( + createSnapshotFn = CreateSnapshot + pushLatestFn = PushLatestSnapshot +) + func PushLatestSnapshot() PushResult { cfg := GetPushConfig() if cfg.ServerURL == "" || cfg.RouterID == "" { diff --git a/internal/modules/snapshot_push_553_test.go b/internal/modules/snapshot_push_553_test.go new file mode 100644 index 00000000..7f208ed3 --- /dev/null +++ b/internal/modules/snapshot_push_553_test.go @@ -0,0 +1,45 @@ +package modules + +import ( + "errors" + "testing" +) + +var errTestSnapshot = errors.New("create failed") + +// #553: CreateAndPushSnapshot crea un snapshot fresco y empuja el último. +// Los pasos son inyectables: sin OpenWrt (sin uci) el create real fallaría. +func TestCreateAndPushSnapshot(t *testing.T) { + oldCreate, oldPush := createSnapshotFn, pushLatestFn + t.Cleanup(func() { createSnapshotFn, pushLatestFn = oldCreate, oldPush }) + + t.Run("create falla y no empuja", func(t *testing.T) { + createSnapshotFn = func() (*ConfigSnapshot, error) { return nil, errTestSnapshot } + pushed := false + pushLatestFn = func() PushResult { pushed = true; return PushResult{Ok: true} } + res := CreateAndPushSnapshot() + if res.Ok || pushed { + t.Fatalf("create falló: got %+v pushed=%v", res, pushed) + } + }) + + t.Run("create ok y empuja el resultado", func(t *testing.T) { + createSnapshotFn = func() (*ConfigSnapshot, error) { + return &ConfigSnapshot{ID: "20261021-000000"}, nil + } + pushLatestFn = func() PushResult { return PushResult{Ok: true, SnapshotID: "20261021-000000"} } + res := CreateAndPushSnapshot() + if !res.Ok || res.SnapshotID != "20261021-000000" { + t.Fatalf("ok: got %+v", res) + } + }) + + t.Run("el push fallo se propaga", func(t *testing.T) { + createSnapshotFn = func() (*ConfigSnapshot, error) { return &ConfigSnapshot{ID: "x"}, nil } + pushLatestFn = func() PushResult { return PushResult{Error: "push failed"} } + res := CreateAndPushSnapshot() + if res.Ok || res.Error != "push failed" { + t.Fatalf("push falló: got %+v", res) + } + }) +}