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