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