blob: 7dc8139ff654c6922611699d2fec67fb8c5bfedc [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanski6f599512023-04-26 19:08:19 +02004package scruffy
5
6import (
7 "context"
8 "database/sql"
9 "testing"
10 "time"
11
12 "github.com/prometheus/client_golang/prometheus"
13 "google.golang.org/protobuf/proto"
14
15 aapi "source.monogon.dev/cloud/agent/api"
Tim Windelschmidt53087302023-06-27 16:36:31 +020016 "source.monogon.dev/cloud/bmaas/server/api"
17
Serge Bazanski6f599512023-04-26 19:08:19 +020018 "source.monogon.dev/cloud/bmaas/bmdb"
19 "source.monogon.dev/cloud/bmaas/bmdb/model"
Serge Bazanski6f599512023-04-26 19:08:19 +020020 "source.monogon.dev/cloud/lib/component"
21)
22
23type filler func(ctx context.Context, q *model.Queries) error
24
25func fill() filler {
26 return func(ctx context.Context, q *model.Queries) error {
27 return nil
28 }
29}
30
31func (f filler) chain(n func(ctx context.Context, q *model.Queries) error) filler {
32 return func(ctx context.Context, q *model.Queries) error {
33 if err := f(ctx, q); err != nil {
34 return err
35 }
36 return n(ctx, q)
37 }
38}
39
40type fillerMachine struct {
41 f filler
42
43 provider *model.Provider
44 providerID *string
45
46 location *string
47
48 threads *int32
49 ramgb *int64
50
51 agentStartedAt *time.Time
52
53 agentHeartbeatAt *time.Time
54
55 installationRequestGeneration *int64
56
57 installationReportGeneration *int64
58}
59
60func (f filler) machine() *fillerMachine {
61 return &fillerMachine{
62 f: f,
63 }
64}
65
66func (m *fillerMachine) provided(p model.Provider, pid string) *fillerMachine {
67 m.provider = &p
68 m.providerID = &pid
69 return m
70}
71
72func (m *fillerMachine) providedE(pid string) *fillerMachine {
73 return m.provided(model.ProviderEquinix, pid)
74}
75
76func (m *fillerMachine) located(location string) *fillerMachine {
77 m.location = &location
78 return m
79}
80
81func (m *fillerMachine) hardware(threads int32, ramgb int64) *fillerMachine {
82 m.threads = &threads
83 m.ramgb = &ramgb
84 return m
85}
86
87func (m *fillerMachine) agentStarted(t time.Time) *fillerMachine {
88 m.agentStartedAt = &t
89 return m
90}
91
92func (m *fillerMachine) agentHeartbeat(t time.Time) *fillerMachine {
93 m.agentHeartbeatAt = &t
94 return m
95}
96
97func (m *fillerMachine) agentHealthy() *fillerMachine {
98 now := time.Now()
99 return m.agentStarted(now.Add(-30 * time.Minute)).agentHeartbeat(now.Add(-1 * time.Minute))
100}
101
102func (m *fillerMachine) agentStoppedHeartbeating() *fillerMachine {
103 now := time.Now()
104 return m.agentStarted(now.Add(-30 * time.Minute)).agentHeartbeat(now.Add(-20 * time.Minute))
105}
106
107func (m *fillerMachine) agentNeverHeartbeat() *fillerMachine {
108 now := time.Now()
109 return m.agentStarted(now.Add(-30 * time.Minute))
110}
111
112func (m *fillerMachine) installRequested(gen int64) *fillerMachine {
113 m.installationRequestGeneration = &gen
114 return m
115}
116
117func (m *fillerMachine) installReported(gen int64) *fillerMachine {
118 m.installationReportGeneration = &gen
119 return m
120}
121
122func (m *fillerMachine) build() filler {
123 return m.f.chain(func(ctx context.Context, q *model.Queries) error {
124 mach, err := q.NewMachine(ctx)
125 if err != nil {
126 return err
127 }
128 if m.providerID != nil {
129 err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
130 MachineID: mach.MachineID,
131 Provider: *m.provider,
132 ProviderID: *m.providerID,
133 })
134 if err != nil {
135 return err
136 }
137 if m.location != nil {
138 err = q.MachineUpdateProviderStatus(ctx, model.MachineUpdateProviderStatusParams{
139 ProviderID: *m.providerID,
140 Provider: *m.provider,
141 ProviderLocation: sql.NullString{Valid: true, String: *m.location},
142 })
143 if err != nil {
144 return err
145 }
146 }
147 }
148 if m.threads != nil {
149 report := api.AgentHardwareReport{
150 Report: &aapi.Node{
151 MemoryInstalledBytes: *m.ramgb << 30,
152 MemoryUsableRatio: 1.0,
153 Cpu: []*aapi.CPU{
154 {
155 HardwareThreads: *m.threads,
156 Cores: *m.threads,
157 },
158 },
159 },
160 Warning: nil,
161 }
162 raw, err := proto.Marshal(&report)
163 if err != nil {
164 return err
165 }
166 err = q.MachineSetHardwareReport(ctx, model.MachineSetHardwareReportParams{
167 MachineID: mach.MachineID,
168 HardwareReportRaw: raw,
169 })
170 if err != nil {
171 return err
172 }
173 }
174 if m.agentStartedAt != nil {
175 err = q.MachineSetAgentStarted(ctx, model.MachineSetAgentStartedParams{
176 MachineID: mach.MachineID,
177 AgentStartedAt: *m.agentStartedAt,
178 AgentPublicKey: []byte("fakefakefake"),
179 })
180 if err != nil {
181 return err
182 }
183 }
184 if m.agentHeartbeatAt != nil {
185 err = q.MachineSetAgentHeartbeat(ctx, model.MachineSetAgentHeartbeatParams{
186 MachineID: mach.MachineID,
187 AgentHeartbeatAt: *m.agentHeartbeatAt,
188 })
189 if err != nil {
190 return err
191 }
192 }
193 if m.installationRequestGeneration != nil {
194 err = q.MachineSetOSInstallationRequest(ctx, model.MachineSetOSInstallationRequestParams{
195 MachineID: mach.MachineID,
196 Generation: *m.installationRequestGeneration,
197 })
198 if err != nil {
199 return err
200 }
201 }
202 if m.installationReportGeneration != nil {
203 err = q.MachineSetOSInstallationReport(ctx, model.MachineSetOSInstallationReportParams{
Tim Windelschmidt53087302023-06-27 16:36:31 +0200204 MachineID: mach.MachineID,
205 Generation: *m.installationReportGeneration,
206 OsInstallationResult: model.MachineOsInstallationResultSuccess,
Serge Bazanski6f599512023-04-26 19:08:19 +0200207 })
208 if err != nil {
209 return err
210 }
211 }
212 return nil
213 })
214}
215
216func TestHWStats(t *testing.T) {
217 s := Server{
218 Config: Config{
219 BMDB: bmdb.BMDB{
220 Config: bmdb.Config{
221 Database: component.CockroachConfig{
222 InMemory: true,
223 },
224 },
225 },
226 },
227 }
228
229 registry := prometheus.NewRegistry()
230 runner := newHWStatsRunner(&s, registry)
231
232 ctx, ctxC := context.WithCancel(context.Background())
233 defer ctxC()
234
235 res, err := registry.Gather()
236 if err != nil {
237 t.Fatalf("Gather: %v", err)
238 }
239 if want, got := 0, len(res); want != got {
240 t.Fatalf("Expected no metrics with empty database, got %d", got)
241 }
242
243 conn, err := s.Config.BMDB.Open(true)
244 if err != nil {
245 t.Fatalf("Open: %v", err)
246 }
247 sess, err := conn.StartSession(ctx)
248 if err != nil {
249 t.Fatalf("StartSession: %v", err)
250 }
251 // Populate database with some test data.
252 err = sess.Transact(ctx, func(q *model.Queries) error {
253 f := fill().
254 machine().provided(model.ProviderEquinix, "1").hardware(32, 256).located("dark-bramble").build().
255 machine().provided(model.ProviderEquinix, "2").hardware(32, 256).located("dark-bramble").build().
256 machine().provided(model.ProviderEquinix, "3").hardware(32, 256).located("dark-bramble").build().
257 machine().provided(model.ProviderEquinix, "4").hardware(32, 256).located("brittle-hollow").build().
258 machine().provided(model.ProviderEquinix, "5").hardware(32, 256).located("timber-hearth").build().
259 machine().provided(model.ProviderEquinix, "6").hardware(32, 256).located("timber-hearth").build()
260 return f(ctx, q)
261 })
262 if err != nil {
263 t.Fatalf("Transact: %v", err)
264 }
265
266 s.bmdb = conn
267 s.sessionC = make(chan *bmdb.Session)
268 go s.sessionWorker(ctx)
269
270 // Do a statistics run and check results.
271 if err := runner.runOnce(ctx); err != nil {
272 t.Fatalf("runOnce: %v", err)
273 }
274
275 mfs, err := registry.Gather()
276 if err != nil {
277 t.Fatalf("Gatcher: %v", err)
278 }
279
280 // metric name -> provider -> location -> value
281 values := make(map[string]map[string]map[string]float64)
282 for _, mf := range mfs {
283 values[*mf.Name] = make(map[string]map[string]float64)
284 for _, m := range mf.Metric {
285 var provider, location string
286 for _, pair := range m.Label {
287 switch *pair.Name {
288 case "location":
289 location = *pair.Value
290 case "provider":
291 provider = *pair.Value
292 }
293 }
294 if _, ok := values[*mf.Name][provider]; !ok {
295 values[*mf.Name][provider] = make(map[string]float64)
296 }
297 switch {
298 case m.Gauge != nil && m.Gauge.Value != nil:
299 values[*mf.Name][provider][location] = *m.Gauge.Value
300 }
301 }
302 }
303
304 for _, te := range []struct {
305 provider model.Provider
306 location string
307 threads int32
308 ramgb int64
309 }{
310 {model.ProviderEquinix, "dark-bramble", 96, 768},
311 {model.ProviderEquinix, "brittle-hollow", 32, 256},
312 {model.ProviderEquinix, "timber-hearth", 64, 512},
313 } {
314 threads := values["bmdb_hwstats_region_cpu_threads"][string(te.provider)][te.location]
315 bytes := values["bmdb_hwstats_region_ram_bytes"][string(te.provider)][te.location]
316
317 if want, got := te.threads, int32(threads); want != got {
318 t.Errorf("Wanted %d threads in %s/%s, got %d", want, te.provider, te.location, got)
319 }
320 if want, got := te.ramgb, int64(bytes)>>30; want != got {
321 t.Errorf("Wanted %d GB RAM in %s/%s, got %d", want, te.provider, te.location, got)
322 }
323 }
324}