blob: 17abc9d8e0fe4a40789220c71b21cc733c9c2f50 [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 Bazanski424e2012023-02-15 23:31:49 +01004package bmdb
5
6import (
7 "context"
8 "fmt"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/google/uuid"
Serge Bazanski10b21542023-04-13 12:12:05 +020014 "google.golang.org/protobuf/proto"
Serge Bazanski424e2012023-02-15 23:31:49 +010015
Serge Bazanski10b21542023-04-13 12:12:05 +020016 apb "source.monogon.dev/cloud/agent/api"
Serge Bazanski424e2012023-02-15 23:31:49 +010017 "source.monogon.dev/cloud/bmaas/bmdb/model"
18 "source.monogon.dev/cloud/bmaas/bmdb/reflection"
Serge Bazanski10b21542023-04-13 12:12:05 +020019 "source.monogon.dev/cloud/bmaas/server/api"
Serge Bazanski424e2012023-02-15 23:31:49 +010020)
21
22// TestReflection exercises the BMDB reflection schema reflection and data
23// retrieval code. Ideally this code would live in //cloud/bmaas/bmdb/reflection,
24// but due to namespacing issues it lives here.
25func TestReflection(t *testing.T) {
26 b := dut()
27 conn, err := b.Open(true)
28 if err != nil {
29 t.Fatalf("Open failed: %v", err)
30 }
31
32 ctx, ctxC := context.WithCancel(context.Background())
33 defer ctxC()
34
35 sess, err := conn.StartSession(ctx)
36 if err != nil {
37 t.Fatalf("StartSession: %v", err)
38 }
39
40 // Create 10 test machines.
41 var mids []uuid.UUID
Tim Windelschmidt2a74e582024-04-11 01:37:49 +020042 err = sess.Transact(ctx, func(q *model.Queries) error {
Serge Bazanski424e2012023-02-15 23:31:49 +010043 for i := 0; i < 10; i += 1 {
44 mach, err := q.NewMachine(ctx)
45 if err != nil {
46 return err
47 }
48 err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
49 MachineID: mach.MachineID,
50 Provider: model.ProviderEquinix,
51 ProviderID: fmt.Sprintf("test-%d", i),
52 })
53 if err != nil {
54 return err
55 }
56 mids = append(mids, mach.MachineID)
57 }
58 return nil
59 })
60 if err != nil {
61 t.Fatal(err)
62 }
63 // Start and fail work on one of the machines with an hour long backoff.
64 w, err := sess.Work(ctx, model.ProcessUnitTest1, func(q *model.Queries) ([]uuid.UUID, error) {
65 return mids[0:1], nil
66 })
67 if err != nil {
68 t.Fatal(err)
69 }
Serge Bazanski20312b42023-04-19 13:49:47 +020070 backoff := Backoff{
71 Initial: time.Hour,
72 }
73 w.Fail(ctx, &backoff, "failure test")
Serge Bazanski424e2012023-02-15 23:31:49 +010074
75 // On another machine, create a failure with a 1 second backoff.
76 w, err = sess.Work(ctx, model.ProcessUnitTest1, func(q *model.Queries) ([]uuid.UUID, error) {
77 return mids[1:2], nil
78 })
79 if err != nil {
80 t.Fatal(err)
81 }
Serge Bazanski20312b42023-04-19 13:49:47 +020082 backoff = Backoff{
83 Initial: time.Second,
84 }
85 w.Fail(ctx, &backoff, "failure test")
Serge Bazanski424e2012023-02-15 23:31:49 +010086 // Later on in the test we must wait for this backoff to actually elapse. Start
87 // counting now.
Serge Bazanski20312b42023-04-19 13:49:47 +020088 elapsed := time.NewTicker(time.Second * 1)
Serge Bazanski424e2012023-02-15 23:31:49 +010089 defer elapsed.Stop()
90
91 // On another machine, create work and don't finish it yet.
92 _, err = sess.Work(ctx, model.ProcessUnitTest1, func(q *model.Queries) ([]uuid.UUID, error) {
93 return mids[2:3], nil
94 })
95 if err != nil {
96 t.Fatal(err)
97 }
98
99 schema, err := conn.Reflect(ctx)
100 if err != nil {
101 t.Fatalf("ReflectTagTypes: %v", err)
102 }
103
104 // Dump all in strict mode.
105 opts := &reflection.GetMachinesOpts{
106 Strict: true,
107 }
108 res, err := schema.GetMachines(ctx, opts)
109 if err != nil {
110 t.Fatalf("Dump failed: %v", err)
111 }
112 if res.Query == "" {
113 t.Errorf("Query not set on result")
114 }
115 machines := res.Data
116 if want, got := 10, len(machines); want != got {
117 t.Fatalf("Expected %d machines in dump, got %d", want, got)
118 }
119
120 // Expect Provided tag on all machines. Do a detailed check on fields, too.
121 for _, machine := range machines {
122 tag, ok := machine.Tags["Provided"]
123 if !ok {
124 t.Errorf("No Provided tag on machine.")
125 continue
126 }
127 if want, got := "Provided", tag.Type.Name(); want != got {
128 t.Errorf("Provided tag should have type %q, got %q", want, got)
129 }
130 if provider := tag.Field("provider"); provider != nil {
131 if want, got := provider.HumanValue(), "Equinix"; want != got {
132 t.Errorf("Wanted Provided.provider value %q, got %q", want, got)
133 }
134 } else {
135 t.Errorf("Provider tag has no provider field")
136 }
137 if providerId := tag.Field("provider_id"); providerId != nil {
138 if !strings.HasPrefix(providerId.HumanValue(), "test-") {
139 t.Errorf("Unexpected provider_id value %q", providerId.HumanValue())
140 }
141 } else {
142 t.Errorf("Provider tag has no provider_id field")
143 }
144 }
145
146 // Now just dump one machine.
147 opts.FilterMachine = &mids[0]
148 res, err = schema.GetMachines(ctx, opts)
149 if err != nil {
150 t.Fatalf("Dump failed: %v", err)
151 }
152 machines = res.Data
153 if want, got := 1, len(machines); want != got {
154 t.Fatalf("Expected %d machines in dump, got %d", want, got)
155 }
156 if want, got := mids[0].String(), machines[0].ID.String(); want != got {
157 t.Fatalf("Expected machine %s, got %s", want, got)
158 }
159
160 // Now dump a machine that doesn't exist. That should just return an empty list.
161 fakeMid := uuid.New()
162 opts.FilterMachine = &fakeMid
163 res, err = schema.GetMachines(ctx, opts)
164 if err != nil {
165 t.Fatalf("Dump failed: %v", err)
166 }
167 machines = res.Data
168 if want, got := 0, len(machines); want != got {
169 t.Fatalf("Expected %d machines in dump, got %d", want, got)
170 }
171
172 // Finally, check the special case machines. The first one should have an active
173 // backoff.
174 opts.FilterMachine = &mids[0]
175 res, err = schema.GetMachines(ctx, opts)
176 if err != nil {
177 t.Errorf("Dump failed: %v", err)
178 } else {
179 machine := res.Data[0]
180 if _, ok := machine.Backoffs["UnitTest1"]; !ok {
181 t.Errorf("Expected UnitTest1 backoff on machine")
182 }
183 }
184 // The second one should have an expired backoff that shouldn't be reported in a
185 // normal call..
186 <-elapsed.C
187 opts.FilterMachine = &mids[1]
188 res, err = schema.GetMachines(ctx, opts)
189 if err != nil {
190 t.Errorf("Dump failed: %v", err)
191 } else {
192 machine := res.Data[0]
193 if _, ok := machine.Backoffs["UnitTest1"]; ok {
194 t.Errorf("Expected no UnitTest1 backoff on machine")
195 }
196 }
197 // But if we ask for expired backoffs, we should get it.
198 opts.ExpiredBackoffs = true
199 res, err = schema.GetMachines(ctx, opts)
200 if err != nil {
201 t.Errorf("Dump failed: %v", err)
202 } else {
203 machine := res.Data[0]
204 if _, ok := machine.Backoffs["UnitTest1"]; !ok {
205 t.Errorf("Expected UnitTest1 backoff on machine")
206 }
207 }
208 // Finally, the third machine should have an active Work item.
209 opts.FilterMachine = &mids[2]
210 res, err = schema.GetMachines(ctx, opts)
211 if err != nil {
212 t.Errorf("Dump failed: %v", err)
213 } else {
214 machine := res.Data[0]
215 if _, ok := machine.Work["UnitTest1"]; !ok {
216 t.Errorf("Expected UnitTest1 work item on machine")
217 }
218 }
219}
Serge Bazanski10b21542023-04-13 12:12:05 +0200220
221// TestReflectionProtoFields ensures that the basic proto field introspection
222// functionality works.
223func TestReflectionProtoFields(t *testing.T) {
224 s := dut()
225 ctx, ctxC := context.WithCancel(context.Background())
226 defer ctxC()
227
228 bmdb, err := s.Open(true)
229 if err != nil {
230 t.Fatalf("Open: %v", err)
231 }
232 sess, err := bmdb.StartSession(ctx)
233 if err != nil {
234 t.Fatalf("StartSession: %v", err)
235 }
236 var machine model.Machine
237 err = sess.Transact(ctx, func(q *model.Queries) error {
238 machine, err = q.NewMachine(ctx)
239 if err != nil {
240 return err
241 }
242
243 report := &api.AgentHardwareReport{
244 Report: &apb.Node{
245 Manufacturer: "Charles Babbage",
246 Product: "Analytical Engine",
247 SerialNumber: "183701",
248 MemoryInstalledBytes: 14375,
249 MemoryUsableRatio: 1.0,
250 Cpu: []*apb.CPU{
251 {
252 Architecture: nil,
253 HardwareThreads: 1,
254 Cores: 1,
255 },
256 },
257 },
258 Warning: []string{"something went wrong"},
259 }
260 b, _ := proto.Marshal(report)
261 return q.MachineSetHardwareReport(ctx, model.MachineSetHardwareReportParams{
262 MachineID: machine.MachineID,
263 HardwareReportRaw: b,
264 })
265 })
266 if err != nil {
267 t.Fatalf("Failed to submit hardware report: %v", err)
268 }
269
270 schem, err := bmdb.Reflect(ctx)
271 if err != nil {
272 t.Fatalf("Failed to reflect on database: %v", err)
273 }
274
275 machines, err := schem.GetMachines(ctx, &reflection.GetMachinesOpts{FilterMachine: &machine.MachineID, Strict: true})
276 if err != nil {
277 t.Fatalf("Failed to get machine: %v", err)
278 }
279 if len(machines.Data) != 1 {
280 t.Errorf("Expected one machine, got %d", len(machines.Data))
281 } else {
282 machine := machines.Data[0]
283 ty := machine.Tags["HardwareReport"].Field("hardware_report_raw").Type.HumanType()
284 if want, got := "cloud.bmaas.server.api.AgentHardwareReport", ty; want != got {
285 t.Errorf("Mismatch in type: wanted %q, got %q", want, got)
286 }
287 v := machine.Tags["HardwareReport"].Field("hardware_report_raw").HumanValue()
288 if !strings.Contains(v, "manufacturer:") {
289 t.Errorf("Invalid serialized prototext: %s", v)
290 }
Serge Bazanski3c6306b2023-09-19 11:48:44 +0000291 fv, err := machine.Tags["HardwareReport"].Field("hardware_report_raw").Index("report.cpu[0].cores")
292 if err != nil {
293 t.Errorf("Could not get report.cpu[0].cores from hardware_report_raw: %v", err)
294 } else {
295 if want, got := "1", fv; want != got {
296 t.Errorf("report.cpu[0].cores should be %q, got %q", want, got)
297 }
298 }
Serge Bazanski10b21542023-04-13 12:12:05 +0200299 }
300}