mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 17:07:47 +02:00
160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
//go:build testing
|
|
|
|
package systems
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"net"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/henrygd/beszel/internal/common"
|
|
"github.com/henrygd/beszel/internal/entities/monitor"
|
|
esystem "github.com/henrygd/beszel/internal/entities/system"
|
|
"github.com/henrygd/beszel/internal/hub/expirymap"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestSSHNetworkMonitorReconnectSync(t *testing.T) {
|
|
sys, app := newTestSystemWithHub(t)
|
|
sys.manager.zfsFetchMap = expirymap.New[zfsFetchState](time.Hour)
|
|
t.Cleanup(sys.manager.zfsFetchMap.StopCleaner)
|
|
sys.ctx = context.Background()
|
|
sys.Status = up
|
|
_, key, err := ed25519.GenerateKey(rand.Reader)
|
|
require.NoError(t, err)
|
|
signer, err := ssh.NewSignerFromKey(key)
|
|
require.NoError(t, err)
|
|
config := &ssh.ServerConfig{NoClientAuth: true, ServerVersion: "SSH-2.0-beszel_0.20.0"}
|
|
config.AddHostKey(signer)
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = listener.Close() })
|
|
sys.Host, sys.Port, err = net.SplitHostPort(listener.Addr().String())
|
|
require.NoError(t, err)
|
|
sys.manager.sshConfig = &ssh.ClientConfig{User: "test", HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: time.Second}
|
|
t.Cleanup(sys.closeSSHConnection)
|
|
requests := make(chan monitor.SyncRequest, 10)
|
|
var failSync atomic.Bool
|
|
go func() {
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func() {
|
|
server, channels, reqs, err := ssh.NewServerConn(conn, config)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
defer server.Close()
|
|
go ssh.DiscardRequests(reqs)
|
|
for channel := range channels {
|
|
ch, reqs, err := channel.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func() {
|
|
defer ch.Close()
|
|
for req := range reqs {
|
|
if req.Type != "shell" {
|
|
_ = req.Reply(false, nil)
|
|
continue
|
|
}
|
|
_ = req.Reply(true, nil)
|
|
var request common.HubRequest[cbor.RawMessage]
|
|
if cbor.NewDecoder(ch).Decode(&request) != nil {
|
|
return
|
|
}
|
|
response := common.AgentResponse{}
|
|
switch request.Action {
|
|
case common.GetData:
|
|
response.SystemData = &esystem.CombinedData{}
|
|
case common.SyncNetworkMonitors:
|
|
var syncReq monitor.SyncRequest
|
|
if cbor.Unmarshal(request.Data, &syncReq) != nil {
|
|
return
|
|
}
|
|
requests <- syncReq
|
|
if failSync.Load() {
|
|
response.Error = "test sync failure"
|
|
} else {
|
|
response.Data, _ = cbor.Marshal(monitor.SyncResponse{})
|
|
}
|
|
}
|
|
_ = cbor.NewEncoder(ch).Encode(response)
|
|
_, _ = ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{0}))
|
|
return
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
collection, err := app.FindCachedCollectionByNameOrId("network_monitors")
|
|
require.NoError(t, err)
|
|
probe := core.NewRecord(collection)
|
|
probe.Load(map[string]any{"system": sys.Id, "target": "localhost", "protocol": "tcp", "port": 80, "interval": 60, "enabled": true})
|
|
require.NoError(t, app.SaveNoValidate(probe))
|
|
fetch := func() {
|
|
t.Helper()
|
|
_, err := sys.fetchDataFromAgent(common.DataRequestOptions{})
|
|
require.NoError(t, err, "monitor sync failure must not fail stats fetching")
|
|
}
|
|
receive := func() monitor.SyncRequest {
|
|
t.Helper()
|
|
select {
|
|
case req := <-requests:
|
|
require.Equal(t, monitor.SyncActionReplace, req.Action)
|
|
return req
|
|
case <-time.After(time.Second):
|
|
t.Fatal("missing full monitor sync")
|
|
return monitor.SyncRequest{}
|
|
}
|
|
}
|
|
fetch()
|
|
require.Equal(t, probe.Id, receive().Configs[0].ID)
|
|
require.False(t, sys.monitorsNeedSync.Load())
|
|
fetch()
|
|
require.Empty(t, requests, "steady-state fetch must not resync")
|
|
|
|
// Simulate loss of the agent process/connection and its in-memory monitors.
|
|
require.NoError(t, sys.client.Load().Close())
|
|
fetch()
|
|
require.Equal(t, probe.Id, receive().Configs[0].ID)
|
|
require.False(t, sys.monitorsNeedSync.Load())
|
|
|
|
// Failed replacements are retried on the next successful stats fetch.
|
|
require.NoError(t, sys.client.Load().Close())
|
|
failSync.Store(true)
|
|
fetch()
|
|
receive()
|
|
require.True(t, sys.monitorsNeedSync.Load())
|
|
failSync.Store(false)
|
|
fetch()
|
|
receive()
|
|
require.False(t, sys.monitorsNeedSync.Load())
|
|
|
|
probe.Set("enabled", false)
|
|
require.NoError(t, app.SaveNoValidate(probe))
|
|
require.NoError(t, sys.client.Load().Close())
|
|
fetch()
|
|
require.Empty(t, receive().Configs, "empty replacement must clear stale monitors")
|
|
}
|
|
|
|
func TestPendingNetworkMonitorSyncQueryFailure(t *testing.T) {
|
|
sys, app := newTestSystemWithHub(t)
|
|
_, err := app.DB().NewQuery("DROP TABLE network_monitors").Execute()
|
|
require.NoError(t, err)
|
|
sys.monitorsNeedSync.Store(true)
|
|
sys.syncPendingNetworkMonitors()
|
|
require.True(t, sys.monitorsNeedSync.Load())
|
|
}
|