blob: 84fd61105ef75eb43ce16cdd09beef22a59c78b0 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brunaadeb792023-03-27 15:53:56 +02004package e2e
5
6import (
7 "bufio"
8 "context"
9 "crypto/ed25519"
10 "crypto/rand"
11 "crypto/tls"
12 "crypto/x509"
13 "crypto/x509/pkix"
14 "fmt"
15 "io"
16 "math/big"
17 "net"
18 "net/http"
19 "net/url"
20 "os"
21 "os/exec"
22 "strings"
23 "testing"
24 "time"
25
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010026 "github.com/bazelbuild/rules_go/go/runfiles"
Lorenz Brunaadeb792023-03-27 15:53:56 +020027 "github.com/cavaliergopher/cpio"
Lorenz Brun62f1d362023-11-14 16:18:24 +010028 "github.com/klauspost/compress/zstd"
Lorenz Brunaadeb792023-03-27 15:53:56 +020029 "golang.org/x/sys/unix"
30 "google.golang.org/grpc"
31 "google.golang.org/grpc/credentials"
32 "google.golang.org/protobuf/proto"
33
34 apb "source.monogon.dev/cloud/agent/api"
35 bpb "source.monogon.dev/cloud/bmaas/server/api"
Jan Schär0af4dab2024-04-11 15:04:12 +020036 mpb "source.monogon.dev/metropolis/proto/api"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020037 "source.monogon.dev/osbase/pki"
Lorenz Brunaadeb792023-03-27 15:53:56 +020038)
39
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000040var (
41 // These are filled by bazel at linking time with the canonical path of
42 // their corresponding file. Inside the init function we resolve it
43 // with the rules_go runfiles package to the real path.
44 xBundleFilePath string
45 xOvmfVarsPath string
46 xOvmfCodePath string
47 xKernelPath string
48 xInitramfsOrigPath string
49)
50
51func init() {
52 var err error
53 for _, path := range []*string{
54 &xBundleFilePath, &xOvmfVarsPath, &xOvmfCodePath,
55 &xKernelPath, &xInitramfsOrigPath,
56 } {
57 *path, err = runfiles.Rlocation(*path)
58 if err != nil {
59 panic(err)
60 }
61 }
62}
63
Lorenz Brunaadeb792023-03-27 15:53:56 +020064type fakeServer struct {
65 hardwareReport *bpb.AgentHardwareReport
66 installationRequest *bpb.OSInstallationRequest
67 installationReport *bpb.OSInstallationReport
68}
69
70func (f *fakeServer) Heartbeat(ctx context.Context, req *bpb.AgentHeartbeatRequest) (*bpb.AgentHeartbeatResponse, error) {
71 var res bpb.AgentHeartbeatResponse
72 if req.HardwareReport != nil {
73 f.hardwareReport = req.HardwareReport
74 }
75 if req.InstallationReport != nil {
76 f.installationReport = req.InstallationReport
77 }
78 if f.installationRequest != nil {
79 res.InstallationRequest = f.installationRequest
80 }
81 return &res, nil
82}
83
84const GiB = 1024 * 1024 * 1024
85
86// TestMetropolisInstallE2E exercises the agent communicating against a test cloud
87// API server. This server requests the installation of the Metropolis 'TestOS',
88// which we then validate by looking for a string it outputs on boot.
89func TestMetropolisInstallE2E(t *testing.T) {
90 var f fakeServer
91
92 // Address inside fake QEMU userspace networking
93 grpcAddr := net.TCPAddr{
94 IP: net.IPv4(10, 42, 0, 5),
95 Port: 3000,
96 }
97
98 blobAddr := net.TCPAddr{
99 IP: net.IPv4(10, 42, 0, 6),
100 Port: 80,
101 }
102
103 f.installationRequest = &bpb.OSInstallationRequest{
104 Generation: 5,
105 Type: &bpb.OSInstallationRequest_Metropolis{Metropolis: &bpb.MetropolisInstallationRequest{
106 BundleUrl: (&url.URL{Scheme: "http", Host: blobAddr.String(), Path: "/bundle.bin"}).String(),
107 NodeParameters: &mpb.NodeParameters{},
108 RootDevice: "vda",
109 }},
110 }
111
112 caPubKey, caPrivKey, err := ed25519.GenerateKey(rand.Reader)
113 if err != nil {
114 t.Fatal(err)
115 }
116
117 caCertTmpl := x509.Certificate{
118 SerialNumber: big.NewInt(1),
119 Subject: pkix.Name{
120 CommonName: "Agent E2E Test CA",
121 },
122 NotBefore: time.Now(),
123 NotAfter: pki.UnknownNotAfter,
124 IsCA: true,
125 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
126 BasicConstraintsValid: true,
127 }
128 caCertRaw, err := x509.CreateCertificate(rand.Reader, &caCertTmpl, &caCertTmpl, caPubKey, caPrivKey)
129 if err != nil {
130 t.Fatal(err)
131 }
132 caCert, err := x509.ParseCertificate(caCertRaw)
133 if err != nil {
134 t.Fatal(err)
135 }
136
137 serverPubKey, serverPrivKey, err := ed25519.GenerateKey(rand.Reader)
138 if err != nil {
139 t.Fatal(err)
140 }
141 serverCertTmpl := x509.Certificate{
142 SerialNumber: big.NewInt(1),
143 Subject: pkix.Name{},
144 NotBefore: time.Now(),
145 NotAfter: pki.UnknownNotAfter,
146 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
147 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
148 IPAddresses: []net.IP{grpcAddr.IP},
149 BasicConstraintsValid: true,
150 }
151 serverCert, err := x509.CreateCertificate(rand.Reader, &serverCertTmpl, caCert, serverPubKey, caPrivKey)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200152 if err != nil {
153 t.Fatal(err)
154 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200155
156 s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&tls.Certificate{
157 Certificate: [][]byte{serverCert},
158 PrivateKey: serverPrivKey,
159 })))
160 bpb.RegisterAgentCallbackServer(s, &f)
161 grpcLis, err := net.Listen("tcp", "127.0.0.1:0")
162 if err != nil {
163 panic(err)
164 }
165 go s.Serve(grpcLis)
166 grpcListenAddr := grpcLis.Addr().(*net.TCPAddr)
167
168 m := http.NewServeMux()
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000169
Lorenz Brunaadeb792023-03-27 15:53:56 +0200170 m.HandleFunc("/bundle.bin", func(w http.ResponseWriter, req *http.Request) {
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000171 http.ServeFile(w, req, xBundleFilePath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200172 })
173 blobLis, err := net.Listen("tcp", "127.0.0.1:0")
174 if err != nil {
175 t.Fatal(err)
176 }
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200177
Lorenz Brunaadeb792023-03-27 15:53:56 +0200178 blobListenAddr := blobLis.Addr().(*net.TCPAddr)
179 go http.Serve(blobLis, m)
180
181 _, privateKey, err := ed25519.GenerateKey(rand.Reader)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200182 if err != nil {
183 t.Fatal(err)
184 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200185
186 init := apb.AgentInit{
187 TakeoverInit: &apb.TakeoverInit{
188 MachineId: "testbox1",
189 BmaasEndpoint: grpcAddr.String(),
190 CaCertificate: caCertRaw,
191 },
192 PrivateKey: privateKey,
193 }
194
195 rootDisk, err := os.CreateTemp("", "rootdisk")
196 if err != nil {
197 t.Fatal(err)
198 }
199 defer os.Remove(rootDisk.Name())
Lorenz Brun35fcf032023-06-29 04:15:58 +0200200 // Create a 10GiB sparse root disk
201 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200202 t.Fatalf("ftruncate failed: %v", err)
203 }
204
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000205 initramfsOrigFile, err := os.Open(xInitramfsOrigPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200206 if err != nil {
207 t.Fatal(err)
208 }
209 defer initramfsOrigFile.Close()
210
211 initramfsFile, err := os.CreateTemp("", "agent-initramfs")
212 if err != nil {
213 t.Fatal(err)
214 }
215 defer os.Remove(initramfsFile.Name())
216 if _, err := initramfsFile.ReadFrom(initramfsOrigFile); err != nil {
217 t.Fatal(err)
218 }
219
220 // Append AgentInit spec to initramfs
221 agentInitRaw, err := proto.Marshal(&init)
222 if err != nil {
223 t.Fatal(err)
224 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100225 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
226 if err != nil {
227 t.Fatal(err)
228 }
229 cpioW := cpio.NewWriter(compressedW)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200230 cpioW.WriteHeader(&cpio.Header{
231 Name: "/init.pb",
232 Size: int64(len(agentInitRaw)),
233 Mode: cpio.TypeReg | 0o644,
234 })
235 cpioW.Write(agentInitRaw)
236 cpioW.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +0100237 compressedW.Close()
Lorenz Brunaadeb792023-03-27 15:53:56 +0200238
239 grpcGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", grpcAddr.String(), grpcListenAddr.Port)
240 blobGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", blobAddr.String(), blobListenAddr.Port)
241
242 ovmfVars, err := os.CreateTemp("", "agent-ovmf-vars")
243 if err != nil {
244 t.Fatal(err)
245 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000246 ovmfVarsTmpl, err := os.Open(xOvmfVarsPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200247 if err != nil {
248 t.Fatal(err)
249 }
250 if _, err := io.Copy(ovmfVars, ovmfVarsTmpl); err != nil {
251 t.Fatal(err)
252 }
253
254 qemuArgs := []string{
255 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
256 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000257 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200258 "-drive", "if=pflash,format=raw,file=" + ovmfVars.Name(),
259 "-drive", "if=virtio,format=raw,cache=unsafe,file=" + rootDisk.Name(),
260 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,%s,%s", grpcGuestFwd, blobGuestFwd),
261 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
262 "-device", "virtio-rng-pci",
263 "-serial", "stdio",
264 "-no-reboot",
265 }
266 stage1Args := append(qemuArgs,
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000267 "-kernel", xKernelPath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200268 "-initrd", initramfsFile.Name(),
269 "-append", "console=ttyS0 quiet")
270 qemuCmdAgent := exec.Command("qemu-system-x86_64", stage1Args...)
271 qemuCmdAgent.Stdout = os.Stdout
272 qemuCmdAgent.Stderr = os.Stderr
273 qemuCmdAgent.Run()
274 qemuCmdLaunch := exec.Command("qemu-system-x86_64", qemuArgs...)
275 stdoutPipe, err := qemuCmdLaunch.StdoutPipe()
276 if err != nil {
277 t.Fatal(err)
278 }
279 testosStarted := make(chan struct{})
280 go func() {
281 s := bufio.NewScanner(stdoutPipe)
282 for s.Scan() {
283 if strings.HasPrefix(s.Text(), "[") {
284 continue
285 }
286 t.Log("vm: " + s.Text())
287 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
288 testosStarted <- struct{}{}
289 break
290 }
291 }
292 qemuCmdLaunch.Wait()
293 }()
294 if err := qemuCmdLaunch.Start(); err != nil {
295 t.Fatal(err)
296 }
297 defer qemuCmdLaunch.Process.Kill()
298 select {
299 case <-testosStarted:
300 // Done, test passed
Jan Schär0af4dab2024-04-11 15:04:12 +0200301 case <-time.After(30 * time.Second):
Lorenz Brunaadeb792023-03-27 15:53:56 +0200302 t.Fatal("Waiting for TestOS launch timed out")
303 }
304}