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