mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 19:41:17 +00:00
343 lines
9.9 KiB
Go
343 lines
9.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
type stubBackupProvider struct {
|
|
backups models.Backups
|
|
pbs []models.PBSInstance
|
|
}
|
|
|
|
func (s *stubBackupProvider) GetBackups() models.Backups {
|
|
return s.backups
|
|
}
|
|
|
|
func (s *stubBackupProvider) GetPBSInstances() []models.PBSInstance {
|
|
return s.pbs
|
|
}
|
|
|
|
type stubStorageProvider struct {
|
|
storage []models.Storage
|
|
ceph []models.CephCluster
|
|
}
|
|
|
|
func (s *stubStorageProvider) GetStorage() []models.Storage {
|
|
return s.storage
|
|
}
|
|
|
|
func (s *stubStorageProvider) GetCephClusters() []models.CephCluster {
|
|
return s.ceph
|
|
}
|
|
|
|
type stubDiskHealthProvider struct {
|
|
hosts []models.Host
|
|
}
|
|
|
|
func (s *stubDiskHealthProvider) GetHosts() []models.Host {
|
|
return s.hosts
|
|
}
|
|
|
|
type stubUpdatesProvider struct {
|
|
pending []ContainerUpdateInfo
|
|
enabled bool
|
|
triggerCalled bool
|
|
lastTriggerHost string
|
|
lastUpdateHost string
|
|
lastUpdateID string
|
|
lastUpdateName string
|
|
triggerStatus DockerCommandStatus
|
|
triggerErr error
|
|
updateStatus DockerCommandStatus
|
|
updateErr error
|
|
updateCheckEnabled bool
|
|
}
|
|
|
|
func (s *stubUpdatesProvider) GetPendingUpdates(hostID string) []ContainerUpdateInfo {
|
|
s.lastTriggerHost = hostID
|
|
return s.pending
|
|
}
|
|
|
|
func (s *stubUpdatesProvider) TriggerUpdateCheck(hostID string) (DockerCommandStatus, error) {
|
|
s.triggerCalled = true
|
|
s.lastTriggerHost = hostID
|
|
return s.triggerStatus, s.triggerErr
|
|
}
|
|
|
|
func (s *stubUpdatesProvider) UpdateContainer(hostID, containerID, containerName string) (DockerCommandStatus, error) {
|
|
s.lastUpdateHost = hostID
|
|
s.lastUpdateID = containerID
|
|
s.lastUpdateName = containerName
|
|
return s.updateStatus, s.updateErr
|
|
}
|
|
|
|
func (s *stubUpdatesProvider) IsUpdateActionsEnabled() bool {
|
|
return s.enabled
|
|
}
|
|
|
|
func TestExecuteListBackupsAndStorage(t *testing.T) {
|
|
executor := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{}})
|
|
executor.backupProvider = &stubBackupProvider{
|
|
backups: models.Backups{
|
|
PBS: []models.PBSBackup{
|
|
{
|
|
VMID: "100",
|
|
BackupType: "vm",
|
|
BackupTime: time.Unix(1000, 0),
|
|
Instance: "pbs1",
|
|
Datastore: "ds1",
|
|
Size: 1024 * 1024 * 1024,
|
|
Verified: true,
|
|
Protected: true,
|
|
},
|
|
},
|
|
PVE: models.PVEBackups{
|
|
StorageBackups: []models.StorageBackup{
|
|
{
|
|
VMID: 101,
|
|
Time: time.Unix(1100, 0),
|
|
Size: 2 * 1024 * 1024 * 1024,
|
|
Storage: "local",
|
|
},
|
|
},
|
|
BackupTasks: []models.BackupTask{
|
|
{
|
|
VMID: 101,
|
|
Node: "node1",
|
|
Status: "OK",
|
|
StartTime: time.Unix(1200, 0),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
pbs: []models.PBSInstance{
|
|
{
|
|
Name: "pbs1",
|
|
Host: "10.0.0.1",
|
|
Status: "online",
|
|
Datastores: []models.PBSDatastore{
|
|
{
|
|
Name: "ds1",
|
|
Usage: 50.0,
|
|
Free: 1024 * 1024 * 1024,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result, _ := executor.executeListBackups(context.Background(), map[string]interface{}{})
|
|
var backupsResp BackupsResponse
|
|
if err := json.Unmarshal([]byte(result.Content[0].Text), &backupsResp); err != nil {
|
|
t.Fatalf("decode backups response: %v", err)
|
|
}
|
|
if len(backupsResp.PBS) != 1 || backupsResp.PBS[0].SizeGB != 1 {
|
|
t.Fatalf("unexpected PBS backups: %+v", backupsResp.PBS)
|
|
}
|
|
if len(backupsResp.PVE) != 1 || backupsResp.PVE[0].SizeGB != 2 {
|
|
t.Fatalf("unexpected PVE backups: %+v", backupsResp.PVE)
|
|
}
|
|
if len(backupsResp.PBSServers) != 1 || len(backupsResp.PBSServers[0].Datastores) != 1 {
|
|
t.Fatalf("unexpected PBS servers: %+v", backupsResp.PBSServers)
|
|
}
|
|
if len(backupsResp.RecentTasks) != 1 {
|
|
t.Fatalf("unexpected recent tasks: %+v", backupsResp.RecentTasks)
|
|
}
|
|
|
|
executor.storageProvider = &stubStorageProvider{
|
|
storage: []models.Storage{
|
|
{
|
|
ID: "store1",
|
|
Name: "store1",
|
|
Type: "zfs",
|
|
Status: "active",
|
|
Usage: 25.0,
|
|
Used: 1024 * 1024 * 1024,
|
|
Total: 4 * 1024 * 1024 * 1024,
|
|
Free: 3 * 1024 * 1024 * 1024,
|
|
Content: "images",
|
|
Shared: false,
|
|
ZFSPool: &models.ZFSPool{
|
|
Name: "tank",
|
|
State: "ONLINE",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
Scan: "scrub",
|
|
},
|
|
},
|
|
},
|
|
ceph: []models.CephCluster{
|
|
{
|
|
Name: "ceph1",
|
|
Health: "HEALTH_OK",
|
|
HealthMessage: "ok",
|
|
UsagePercent: 12.5,
|
|
UsedBytes: 2 * 1024 * 1024 * 1024 * 1024,
|
|
TotalBytes: 4 * 1024 * 1024 * 1024 * 1024,
|
|
NumOSDs: 3,
|
|
NumOSDsUp: 3,
|
|
NumOSDsIn: 3,
|
|
NumMons: 1,
|
|
NumMgrs: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
result, _ = executor.executeListStorage(context.Background(), map[string]interface{}{})
|
|
var storageResp StorageResponse
|
|
if err := json.Unmarshal([]byte(result.Content[0].Text), &storageResp); err != nil {
|
|
t.Fatalf("decode storage response: %v", err)
|
|
}
|
|
if len(storageResp.Pools) != 1 || storageResp.Pools[0].ZFS == nil {
|
|
t.Fatalf("unexpected storage pools: %+v", storageResp.Pools)
|
|
}
|
|
// Verify UsagePercent is passed through directly (not double-multiplied).
|
|
// Storage.Usage is already in 0-100 range from safePercentage().
|
|
if storageResp.Pools[0].UsagePercent != 25.0 {
|
|
t.Errorf("storage pool UsagePercent = %v, want 25.0 (was double-multiplied before fix)", storageResp.Pools[0].UsagePercent)
|
|
}
|
|
if len(storageResp.CephClusters) != 1 || storageResp.CephClusters[0].UsedTB != 2 {
|
|
t.Fatalf("unexpected ceph clusters: %+v", storageResp.CephClusters)
|
|
}
|
|
}
|
|
|
|
func TestStorageUsagePercentNotDoubled(t *testing.T) {
|
|
// Regression test: Usage is already 0-100 from safePercentage().
|
|
// The bug was multiplying by 100 again, turning 25% into 2500%.
|
|
executor := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{}})
|
|
|
|
// PBS datastore path
|
|
executor.backupProvider = &stubBackupProvider{
|
|
pbs: []models.PBSInstance{
|
|
{
|
|
Name: "pbs1",
|
|
Host: "10.0.0.1",
|
|
Status: "online",
|
|
Datastores: []models.PBSDatastore{
|
|
{Name: "ds1", Usage: 12.3, Free: 1024 * 1024 * 1024},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
result, _ := executor.executeListBackups(context.Background(), map[string]interface{}{})
|
|
var backupsResp BackupsResponse
|
|
if err := json.Unmarshal([]byte(result.Content[0].Text), &backupsResp); err != nil {
|
|
t.Fatalf("decode backups response: %v", err)
|
|
}
|
|
if len(backupsResp.PBSServers) != 1 || len(backupsResp.PBSServers[0].Datastores) != 1 {
|
|
t.Fatalf("unexpected PBS servers: %+v", backupsResp.PBSServers)
|
|
}
|
|
got := backupsResp.PBSServers[0].Datastores[0].UsagePercent
|
|
if got != 12.3 {
|
|
t.Errorf("PBS datastore UsagePercent = %v, want 12.3 (double-multiplied = %v)", got, 12.3*100)
|
|
}
|
|
|
|
// Storage pool path
|
|
executor.storageProvider = &stubStorageProvider{
|
|
storage: []models.Storage{
|
|
{
|
|
ID: "s1", Name: "s1", Type: "dir", Status: "active",
|
|
Usage: 45.7,
|
|
Used: 1024 * 1024 * 1024, Total: 4 * 1024 * 1024 * 1024, Free: 3 * 1024 * 1024 * 1024,
|
|
},
|
|
},
|
|
}
|
|
result2, _ := executor.executeListStorage(context.Background(), map[string]interface{}{})
|
|
var storageResp StorageResponse
|
|
if err := json.Unmarshal([]byte(result2.Content[0].Text), &storageResp); err != nil {
|
|
t.Fatalf("decode storage response: %v", err)
|
|
}
|
|
if len(storageResp.Pools) != 1 {
|
|
t.Fatalf("unexpected pools: %+v", storageResp.Pools)
|
|
}
|
|
got2 := storageResp.Pools[0].UsagePercent
|
|
if got2 != 45.7 {
|
|
t.Errorf("storage pool UsagePercent = %v, want 45.7 (double-multiplied = %v)", got2, 45.7*100)
|
|
}
|
|
}
|
|
|
|
func TestDockerUpdateTools(t *testing.T) {
|
|
state := models.StateSnapshot{
|
|
DockerHosts: []models.DockerHost{
|
|
{
|
|
ID: "host1",
|
|
Hostname: "docker1",
|
|
DisplayName: "Docker One",
|
|
Containers: []models.DockerContainer{
|
|
{ID: "c1", Name: "/nginx"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
stateProv := &mockStateProvider{}
|
|
stateProv.On("GetState").Return(state)
|
|
executor := NewPulseToolExecutor(ExecutorConfig{
|
|
StateProvider: stateProv,
|
|
ControlLevel: ControlLevelControlled,
|
|
})
|
|
updates := &stubUpdatesProvider{
|
|
pending: []ContainerUpdateInfo{
|
|
{HostID: "host1", ContainerID: "c1", ContainerName: "nginx", UpdateAvailable: true},
|
|
},
|
|
enabled: true,
|
|
triggerStatus: DockerCommandStatus{
|
|
ID: "cmd1",
|
|
Type: "check",
|
|
Status: "queued",
|
|
},
|
|
updateStatus: DockerCommandStatus{
|
|
ID: "cmd2",
|
|
Type: "update",
|
|
Status: "queued",
|
|
},
|
|
}
|
|
executor.updatesProvider = updates
|
|
|
|
result, _ := executor.executeListDockerUpdates(context.Background(), map[string]interface{}{"host": "Docker One"})
|
|
var listResp DockerUpdatesResponse
|
|
if err := json.Unmarshal([]byte(result.Content[0].Text), &listResp); err != nil {
|
|
t.Fatalf("decode docker updates: %v", err)
|
|
}
|
|
if listResp.HostID != "host1" || listResp.Total != 1 {
|
|
t.Fatalf("unexpected docker updates response: %+v", listResp)
|
|
}
|
|
|
|
result, _ = executor.executeCheckDockerUpdates(context.Background(), map[string]interface{}{"host": "Docker One"})
|
|
var checkResp DockerCheckUpdatesResponse
|
|
if err := json.Unmarshal([]byte(result.Content[0].Text), &checkResp); err != nil {
|
|
t.Fatalf("decode docker check response: %v", err)
|
|
}
|
|
if checkResp.CommandID != "cmd1" || checkResp.HostID != "host1" {
|
|
t.Fatalf("unexpected check response: %+v", checkResp)
|
|
}
|
|
|
|
executor.controlLevel = ControlLevelAutonomous
|
|
result, _ = executor.executeUpdateDockerContainer(context.Background(), map[string]interface{}{
|
|
"host": "Docker One",
|
|
"container": "c1",
|
|
})
|
|
var updateResp DockerUpdateContainerResponse
|
|
if err := json.Unmarshal([]byte(result.Content[0].Text), &updateResp); err != nil {
|
|
t.Fatalf("decode update response: %v", err)
|
|
}
|
|
if updates.lastUpdateName != "nginx" || updateResp.CommandID != "cmd2" {
|
|
t.Fatalf("unexpected update response: %+v", updateResp)
|
|
}
|
|
|
|
updates.enabled = false
|
|
result, _ = executor.executeUpdateDockerContainer(context.Background(), map[string]interface{}{
|
|
"host": "Docker One",
|
|
"container": "c1",
|
|
})
|
|
if !strings.Contains(result.Content[0].Text, "updates are disabled") {
|
|
t.Fatalf("unexpected disabled response: %s", result.Content[0].Text)
|
|
}
|
|
}
|