blob: dbcab2d92840c6023c90cff97d64fbf0b9c97f5c [file] [log] [blame]
package scruffy
import (
"context"
"database/sql"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/protobuf/proto"
aapi "source.monogon.dev/cloud/agent/api"
"source.monogon.dev/cloud/bmaas/server/api"
"source.monogon.dev/cloud/bmaas/bmdb"
"source.monogon.dev/cloud/bmaas/bmdb/model"
"source.monogon.dev/cloud/lib/component"
)
type filler func(ctx context.Context, q *model.Queries) error
func fill() filler {
return func(ctx context.Context, q *model.Queries) error {
return nil
}
}
func (f filler) chain(n func(ctx context.Context, q *model.Queries) error) filler {
return func(ctx context.Context, q *model.Queries) error {
if err := f(ctx, q); err != nil {
return err
}
return n(ctx, q)
}
}
type fillerMachine struct {
f filler
provider *model.Provider
providerID *string
location *string
threads *int32
ramgb *int64
agentStartedAt *time.Time
agentHeartbeatAt *time.Time
installationRequestGeneration *int64
installationReportGeneration *int64
}
func (f filler) machine() *fillerMachine {
return &fillerMachine{
f: f,
}
}
func (m *fillerMachine) provided(p model.Provider, pid string) *fillerMachine {
m.provider = &p
m.providerID = &pid
return m
}
func (m *fillerMachine) providedE(pid string) *fillerMachine {
return m.provided(model.ProviderEquinix, pid)
}
func (m *fillerMachine) located(location string) *fillerMachine {
m.location = &location
return m
}
func (m *fillerMachine) hardware(threads int32, ramgb int64) *fillerMachine {
m.threads = &threads
m.ramgb = &ramgb
return m
}
func (m *fillerMachine) agentStarted(t time.Time) *fillerMachine {
m.agentStartedAt = &t
return m
}
func (m *fillerMachine) agentHeartbeat(t time.Time) *fillerMachine {
m.agentHeartbeatAt = &t
return m
}
func (m *fillerMachine) agentHealthy() *fillerMachine {
now := time.Now()
return m.agentStarted(now.Add(-30 * time.Minute)).agentHeartbeat(now.Add(-1 * time.Minute))
}
func (m *fillerMachine) agentStoppedHeartbeating() *fillerMachine {
now := time.Now()
return m.agentStarted(now.Add(-30 * time.Minute)).agentHeartbeat(now.Add(-20 * time.Minute))
}
func (m *fillerMachine) agentNeverHeartbeat() *fillerMachine {
now := time.Now()
return m.agentStarted(now.Add(-30 * time.Minute))
}
func (m *fillerMachine) installRequested(gen int64) *fillerMachine {
m.installationRequestGeneration = &gen
return m
}
func (m *fillerMachine) installReported(gen int64) *fillerMachine {
m.installationReportGeneration = &gen
return m
}
func (m *fillerMachine) build() filler {
return m.f.chain(func(ctx context.Context, q *model.Queries) error {
mach, err := q.NewMachine(ctx)
if err != nil {
return err
}
if m.providerID != nil {
err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
MachineID: mach.MachineID,
Provider: *m.provider,
ProviderID: *m.providerID,
})
if err != nil {
return err
}
if m.location != nil {
err = q.MachineUpdateProviderStatus(ctx, model.MachineUpdateProviderStatusParams{
ProviderID: *m.providerID,
Provider: *m.provider,
ProviderLocation: sql.NullString{Valid: true, String: *m.location},
})
if err != nil {
return err
}
}
}
if m.threads != nil {
report := api.AgentHardwareReport{
Report: &aapi.Node{
MemoryInstalledBytes: *m.ramgb << 30,
MemoryUsableRatio: 1.0,
Cpu: []*aapi.CPU{
{
HardwareThreads: *m.threads,
Cores: *m.threads,
},
},
},
Warning: nil,
}
raw, err := proto.Marshal(&report)
if err != nil {
return err
}
err = q.MachineSetHardwareReport(ctx, model.MachineSetHardwareReportParams{
MachineID: mach.MachineID,
HardwareReportRaw: raw,
})
if err != nil {
return err
}
}
if m.agentStartedAt != nil {
err = q.MachineSetAgentStarted(ctx, model.MachineSetAgentStartedParams{
MachineID: mach.MachineID,
AgentStartedAt: *m.agentStartedAt,
AgentPublicKey: []byte("fakefakefake"),
})
if err != nil {
return err
}
}
if m.agentHeartbeatAt != nil {
err = q.MachineSetAgentHeartbeat(ctx, model.MachineSetAgentHeartbeatParams{
MachineID: mach.MachineID,
AgentHeartbeatAt: *m.agentHeartbeatAt,
})
if err != nil {
return err
}
}
if m.installationRequestGeneration != nil {
err = q.MachineSetOSInstallationRequest(ctx, model.MachineSetOSInstallationRequestParams{
MachineID: mach.MachineID,
Generation: *m.installationRequestGeneration,
})
if err != nil {
return err
}
}
if m.installationReportGeneration != nil {
err = q.MachineSetOSInstallationReport(ctx, model.MachineSetOSInstallationReportParams{
MachineID: mach.MachineID,
Generation: *m.installationReportGeneration,
OsInstallationResult: model.MachineOsInstallationResultSuccess,
})
if err != nil {
return err
}
}
return nil
})
}
func TestHWStats(t *testing.T) {
s := Server{
Config: Config{
BMDB: bmdb.BMDB{
Config: bmdb.Config{
Database: component.CockroachConfig{
InMemory: true,
},
},
},
},
}
registry := prometheus.NewRegistry()
runner := newHWStatsRunner(&s, registry)
ctx, ctxC := context.WithCancel(context.Background())
defer ctxC()
res, err := registry.Gather()
if err != nil {
t.Fatalf("Gather: %v", err)
}
if want, got := 0, len(res); want != got {
t.Fatalf("Expected no metrics with empty database, got %d", got)
}
conn, err := s.Config.BMDB.Open(true)
if err != nil {
t.Fatalf("Open: %v", err)
}
sess, err := conn.StartSession(ctx)
if err != nil {
t.Fatalf("StartSession: %v", err)
}
// Populate database with some test data.
err = sess.Transact(ctx, func(q *model.Queries) error {
f := fill().
machine().provided(model.ProviderEquinix, "1").hardware(32, 256).located("dark-bramble").build().
machine().provided(model.ProviderEquinix, "2").hardware(32, 256).located("dark-bramble").build().
machine().provided(model.ProviderEquinix, "3").hardware(32, 256).located("dark-bramble").build().
machine().provided(model.ProviderEquinix, "4").hardware(32, 256).located("brittle-hollow").build().
machine().provided(model.ProviderEquinix, "5").hardware(32, 256).located("timber-hearth").build().
machine().provided(model.ProviderEquinix, "6").hardware(32, 256).located("timber-hearth").build()
return f(ctx, q)
})
if err != nil {
t.Fatalf("Transact: %v", err)
}
s.bmdb = conn
s.sessionC = make(chan *bmdb.Session)
go s.sessionWorker(ctx)
// Do a statistics run and check results.
if err := runner.runOnce(ctx); err != nil {
t.Fatalf("runOnce: %v", err)
}
mfs, err := registry.Gather()
if err != nil {
t.Fatalf("Gatcher: %v", err)
}
// metric name -> provider -> location -> value
values := make(map[string]map[string]map[string]float64)
for _, mf := range mfs {
values[*mf.Name] = make(map[string]map[string]float64)
for _, m := range mf.Metric {
var provider, location string
for _, pair := range m.Label {
switch *pair.Name {
case "location":
location = *pair.Value
case "provider":
provider = *pair.Value
}
}
if _, ok := values[*mf.Name][provider]; !ok {
values[*mf.Name][provider] = make(map[string]float64)
}
switch {
case m.Gauge != nil && m.Gauge.Value != nil:
values[*mf.Name][provider][location] = *m.Gauge.Value
}
}
}
for _, te := range []struct {
provider model.Provider
location string
threads int32
ramgb int64
}{
{model.ProviderEquinix, "dark-bramble", 96, 768},
{model.ProviderEquinix, "brittle-hollow", 32, 256},
{model.ProviderEquinix, "timber-hearth", 64, 512},
} {
threads := values["bmdb_hwstats_region_cpu_threads"][string(te.provider)][te.location]
bytes := values["bmdb_hwstats_region_ram_bytes"][string(te.provider)][te.location]
if want, got := te.threads, int32(threads); want != got {
t.Errorf("Wanted %d threads in %s/%s, got %d", want, te.provider, te.location, got)
}
if want, got := te.ramgb, int64(bytes)>>30; want != got {
t.Errorf("Wanted %d GB RAM in %s/%s, got %d", want, te.provider, te.location, got)
}
}
}