blob: 3bd3df9cb38e5687c6f7787e28ca87d902c25d50 [file] [log] [blame]
Serge Bazanski4abeb132022-10-11 11:32:19 +02001package server
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "testing"
8 "time"
9
10 "github.com/google/uuid"
11 "google.golang.org/grpc"
Serge Bazanski6c9535b2023-01-03 13:17:42 +010012 "google.golang.org/protobuf/proto"
Serge Bazanski4abeb132022-10-11 11:32:19 +020013
Tim Windelschmidt53087302023-06-27 16:36:31 +020014 apb "source.monogon.dev/cloud/bmaas/server/api"
15
Serge Bazanski4abeb132022-10-11 11:32:19 +020016 "source.monogon.dev/cloud/bmaas/bmdb"
17 "source.monogon.dev/cloud/bmaas/bmdb/model"
Serge Bazanski4abeb132022-10-11 11:32:19 +020018 "source.monogon.dev/cloud/lib/component"
19 "source.monogon.dev/metropolis/node/core/rpc"
20)
21
22func dut() *Server {
23 return &Server{
24 Config: Config{
25 Component: component.ComponentConfig{
26 GRPCListenAddress: ":0",
27 DevCerts: true,
28 DevCertsPath: "/tmp/foo",
29 },
30 BMDB: bmdb.BMDB{
31 Config: bmdb.Config{
32 Database: component.CockroachConfig{
33 InMemory: true,
34 },
35 },
36 },
37 PublicListenAddress: ":0",
38 },
39 }
40}
41
42// TestAgentCallbackService exercises the basic flow for submitting an agent
43// heartbeat and hardware report.
44func TestAgentCallbackService(t *testing.T) {
45 s := dut()
46 ctx, ctxC := context.WithCancel(context.Background())
47 defer ctxC()
48 s.Start(ctx)
49
50 pub, priv, err := ed25519.GenerateKey(rand.Reader)
51 if err != nil {
52 t.Fatalf("could not generate keypair: %v", err)
53 }
54
55 sess, err := s.bmdb.StartSession(ctx)
56 if err != nil {
57 t.Fatalf("could not start session")
58 }
59
60 heartbeat := func(mid uuid.UUID) error {
61 creds, err := rpc.NewEphemeralCredentials(priv, nil)
62 if err != nil {
63 t.Fatalf("could not generate ephemeral credentials: %v", err)
64 }
65 conn, err := grpc.Dial(s.ListenPublic, grpc.WithTransportCredentials(creds))
66 if err != nil {
67 t.Fatalf("Dial failed: %v", err)
68 }
69 defer conn.Close()
70
71 stub := apb.NewAgentCallbackClient(conn)
72 _, err = stub.Heartbeat(ctx, &apb.AgentHeartbeatRequest{
73 MachineId: mid.String(),
74 HardwareReport: &apb.AgentHardwareReport{},
75 })
76 return err
77 }
78
79 // First, attempt to heartbeat for some totally made up machine ID. That should
80 // fail.
81 if err := heartbeat(uuid.New()); err == nil {
82 t.Errorf("heartbeat for made up UUID should've failed")
83 }
84
85 // Create an actual machine in the BMDB alongside the expected pubkey within an
86 // AgentStarted tag.
87 var machine model.Machine
88 err = sess.Transact(ctx, func(q *model.Queries) error {
89 machine, err = q.NewMachine(ctx)
90 if err != nil {
91 return err
92 }
93 err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
94 MachineID: machine.MachineID,
95 Provider: model.ProviderEquinix,
96 ProviderID: "123",
97 })
98 if err != nil {
99 return err
100 }
101 return q.MachineSetAgentStarted(ctx, model.MachineSetAgentStartedParams{
102 MachineID: machine.MachineID,
103 AgentStartedAt: time.Now(),
104 AgentPublicKey: pub,
105 })
106 })
107 if err != nil {
108 t.Fatalf("could not create machine: %v", err)
109 }
110
111 // Now heartbeat with correct machine ID and key. This should succeed.
112 if err := heartbeat(machine.MachineID); err != nil {
113 t.Errorf("heartbeat should've succeeded, got: %v", err)
114 }
115
116 // TODO(q3k): test hardware report being attached once we have some debug API
117 // for tags.
118}
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100119
120// TestOSInstallationFlow exercises the agent's OS installation request/report
121// functionality.
122func TestOSInstallationFlow(t *testing.T) {
123 s := dut()
124 ctx, ctxC := context.WithCancel(context.Background())
125 defer ctxC()
126 s.Start(ctx)
127
128 pub, priv, err := ed25519.GenerateKey(rand.Reader)
129 if err != nil {
130 t.Fatalf("could not generate keypair: %v", err)
131 }
132
133 sess, err := s.bmdb.StartSession(ctx)
134 if err != nil {
135 t.Fatalf("could not start session")
136 }
137
138 heartbeat := func(mid uuid.UUID, report *apb.OSInstallationReport) (*apb.AgentHeartbeatResponse, error) {
139 creds, err := rpc.NewEphemeralCredentials(priv, nil)
140 if err != nil {
141 t.Fatalf("could not generate ephemeral credentials: %v", err)
142 }
143 conn, err := grpc.Dial(s.ListenPublic, grpc.WithTransportCredentials(creds))
144 if err != nil {
145 t.Fatalf("Dial failed: %v", err)
146 }
147 defer conn.Close()
148
149 stub := apb.NewAgentCallbackClient(conn)
150 return stub.Heartbeat(ctx, &apb.AgentHeartbeatRequest{
151 MachineId: mid.String(),
152 HardwareReport: &apb.AgentHardwareReport{},
153 InstallationReport: report,
154 })
155 }
156
157 // Create machine with no OS installation request.
158 var machine model.Machine
159 err = sess.Transact(ctx, func(q *model.Queries) error {
160 machine, err = q.NewMachine(ctx)
161 if err != nil {
162 return err
163 }
164 err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
165 MachineID: machine.MachineID,
166 Provider: model.ProviderEquinix,
167 ProviderID: "123",
168 })
169 if err != nil {
170 return err
171 }
172 return q.MachineSetAgentStarted(ctx, model.MachineSetAgentStartedParams{
173 MachineID: machine.MachineID,
174 AgentStartedAt: time.Now(),
175 AgentPublicKey: pub,
176 })
177 })
178 if err != nil {
179 t.Fatalf("could not create machine: %v", err)
180 }
181
182 // Expect successful heartbeat, but no OS installation request.
183 hbr, err := heartbeat(machine.MachineID, nil)
184 if err != nil {
185 t.Fatalf("heartbeat: %v", err)
186 }
187 if hbr.InstallationRequest != nil {
188 t.Fatalf("expected no installation request")
189 }
190
191 // Now add an OS installation request tag, and expect it to be returned.
192 err = sess.Transact(ctx, func(q *model.Queries) error {
193 req := apb.OSInstallationRequest{
194 Generation: 123,
195 }
196 raw, _ := proto.Marshal(&req)
197 return q.MachineSetOSInstallationRequest(ctx, model.MachineSetOSInstallationRequestParams{
198 MachineID: machine.MachineID,
199 Generation: req.Generation,
200 OsInstallationRequestRaw: raw,
201 })
202 })
203 if err != nil {
204 t.Fatalf("could not add os installation request to machine: %v", err)
205 }
206
207 // Heartbeat a few times just to make sure every response is as expected.
208 for i := 0; i < 3; i++ {
209 hbr, err = heartbeat(machine.MachineID, nil)
210 if err != nil {
211 t.Fatalf("heartbeat: %v", err)
212 }
213 if hbr.InstallationRequest == nil || hbr.InstallationRequest.Generation != 123 {
214 t.Fatalf("expected installation request for generation 123, got %+v", hbr.InstallationRequest)
215 }
216 }
217
218 // Submit a report, expect no more request.
Tim Windelschmidt53087302023-06-27 16:36:31 +0200219 hbr, err = heartbeat(machine.MachineID, &apb.OSInstallationReport{Generation: 123, Result: &apb.OSInstallationReport_Success_{}})
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100220 if err != nil {
221 t.Fatalf("heartbeat: %v", err)
222 }
223 if hbr.InstallationRequest != nil {
224 t.Fatalf("expected no installation request")
225 }
226
227 // Submit a newer request, expect it to be returned.
228 err = sess.Transact(ctx, func(q *model.Queries) error {
229 req := apb.OSInstallationRequest{
230 Generation: 234,
231 }
232 raw, _ := proto.Marshal(&req)
233 return q.MachineSetOSInstallationRequest(ctx, model.MachineSetOSInstallationRequestParams{
234 MachineID: machine.MachineID,
235 Generation: req.Generation,
236 OsInstallationRequestRaw: raw,
237 })
238 })
239 if err != nil {
240 t.Fatalf("could not update installation request: %v", err)
241 }
242
243 // Heartbeat a few times just to make sure every response is as expected.
244 for i := 0; i < 3; i++ {
245 hbr, err = heartbeat(machine.MachineID, nil)
246 if err != nil {
247 t.Fatalf("heartbeat: %v", err)
248 }
249 if hbr.InstallationRequest == nil || hbr.InstallationRequest.Generation != 234 {
250 t.Fatalf("expected installation request for generation 234, got %+v", hbr.InstallationRequest)
251 }
252 }
253}