| 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) |
| } |
| } |
| } |