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