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