blob: dc3108e8893ae56b4f1e5eaf9731dba3b1f577b0 [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"
Lorenz Brunaadeb792023-03-27 15:53:56 +020019 "os"
20 "os/exec"
21 "strings"
22 "testing"
23 "time"
24
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010025 "github.com/bazelbuild/rules_go/go/runfiles"
Lorenz Brunaadeb792023-03-27 15:53:56 +020026 "github.com/cavaliergopher/cpio"
Lorenz Brun62f1d362023-11-14 16:18:24 +010027 "github.com/klauspost/compress/zstd"
Lorenz Brunaadeb792023-03-27 15:53:56 +020028 "golang.org/x/sys/unix"
29 "google.golang.org/grpc"
30 "google.golang.org/grpc/credentials"
31 "google.golang.org/protobuf/proto"
32
33 apb "source.monogon.dev/cloud/agent/api"
Jan Schär0af4dab2024-04-11 15:04:12 +020034 mpb "source.monogon.dev/metropolis/proto/api"
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +020035
Jan Schär4cc3d4d2025-04-14 11:46:47 +000036 "source.monogon.dev/osbase/oci"
37 "source.monogon.dev/osbase/oci/registry"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020038 "source.monogon.dev/osbase/pki"
Lorenz Brunaadeb792023-03-27 15:53:56 +020039)
40
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000041var (
42 // These are filled by bazel at linking time with the canonical path of
43 // their corresponding file. Inside the init function we resolve it
44 // with the rules_go runfiles package to the real path.
Jan Schär4cc3d4d2025-04-14 11:46:47 +000045 xImagePath string
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000046 xOvmfVarsPath string
47 xOvmfCodePath string
48 xKernelPath string
49 xInitramfsOrigPath string
50)
51
52func init() {
53 var err error
54 for _, path := range []*string{
Jan Schär4cc3d4d2025-04-14 11:46:47 +000055 &xImagePath, &xOvmfVarsPath, &xOvmfCodePath,
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000056 &xKernelPath, &xInitramfsOrigPath,
57 } {
58 *path, err = runfiles.Rlocation(*path)
59 if err != nil {
60 panic(err)
61 }
62 }
63}
64
Lorenz Brunaadeb792023-03-27 15:53:56 +020065type fakeServer struct {
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +020066 hardwareReport *apb.AgentHardwareReport
67 installationRequest *apb.OSInstallationRequest
68 installationReport *apb.OSInstallationReport
Lorenz Brunaadeb792023-03-27 15:53:56 +020069}
70
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +020071func (f *fakeServer) Heartbeat(ctx context.Context, req *apb.HeartbeatRequest) (*apb.HeartbeatResponse, error) {
72 var res apb.HeartbeatResponse
Lorenz Brunaadeb792023-03-27 15:53:56 +020073 if req.HardwareReport != nil {
74 f.hardwareReport = req.HardwareReport
75 }
76 if req.InstallationReport != nil {
77 f.installationReport = req.InstallationReport
78 }
79 if f.installationRequest != nil {
80 res.InstallationRequest = f.installationRequest
81 }
82 return &res, nil
83}
84
85const GiB = 1024 * 1024 * 1024
86
87// TestMetropolisInstallE2E exercises the agent communicating against a test cloud
88// API server. This server requests the installation of the Metropolis 'TestOS',
89// which we then validate by looking for a string it outputs on boot.
90func TestMetropolisInstallE2E(t *testing.T) {
91 var f fakeServer
92
93 // Address inside fake QEMU userspace networking
94 grpcAddr := net.TCPAddr{
95 IP: net.IPv4(10, 42, 0, 5),
96 Port: 3000,
97 }
98
Jan Schär4cc3d4d2025-04-14 11:46:47 +000099 registryAddr := net.TCPAddr{
Lorenz Brunaadeb792023-03-27 15:53:56 +0200100 IP: net.IPv4(10, 42, 0, 6),
101 Port: 80,
102 }
103
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000104 image, err := oci.ReadLayout(xImagePath)
105 if err != nil {
106 t.Fatal(err)
107 }
108
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +0200109 f.installationRequest = &apb.OSInstallationRequest{
Lorenz Brunaadeb792023-03-27 15:53:56 +0200110 Generation: 5,
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +0200111 Type: &apb.OSInstallationRequest_Metropolis{Metropolis: &apb.MetropolisInstallationRequest{
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000112 OsImage: &mpb.OSImageRef{
113 Scheme: "http",
114 Host: registryAddr.String(),
115 Repository: "testos",
116 Tag: "latest",
117 Digest: image.ManifestDigest,
118 },
Lorenz Brunaadeb792023-03-27 15:53:56 +0200119 NodeParameters: &mpb.NodeParameters{},
120 RootDevice: "vda",
121 }},
122 }
123
124 caPubKey, caPrivKey, err := ed25519.GenerateKey(rand.Reader)
125 if err != nil {
126 t.Fatal(err)
127 }
128
129 caCertTmpl := x509.Certificate{
130 SerialNumber: big.NewInt(1),
131 Subject: pkix.Name{
132 CommonName: "Agent E2E Test CA",
133 },
134 NotBefore: time.Now(),
135 NotAfter: pki.UnknownNotAfter,
136 IsCA: true,
137 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
138 BasicConstraintsValid: true,
139 }
140 caCertRaw, err := x509.CreateCertificate(rand.Reader, &caCertTmpl, &caCertTmpl, caPubKey, caPrivKey)
141 if err != nil {
142 t.Fatal(err)
143 }
144 caCert, err := x509.ParseCertificate(caCertRaw)
145 if err != nil {
146 t.Fatal(err)
147 }
148
149 serverPubKey, serverPrivKey, err := ed25519.GenerateKey(rand.Reader)
150 if err != nil {
151 t.Fatal(err)
152 }
153 serverCertTmpl := x509.Certificate{
154 SerialNumber: big.NewInt(1),
155 Subject: pkix.Name{},
156 NotBefore: time.Now(),
157 NotAfter: pki.UnknownNotAfter,
158 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
159 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
160 IPAddresses: []net.IP{grpcAddr.IP},
161 BasicConstraintsValid: true,
162 }
163 serverCert, err := x509.CreateCertificate(rand.Reader, &serverCertTmpl, caCert, serverPubKey, caPrivKey)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200164 if err != nil {
165 t.Fatal(err)
166 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200167
168 s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&tls.Certificate{
169 Certificate: [][]byte{serverCert},
170 PrivateKey: serverPrivKey,
171 })))
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +0200172 apb.RegisterAgentCallbackServer(s, &f)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200173 grpcLis, err := net.Listen("tcp", "127.0.0.1:0")
174 if err != nil {
175 panic(err)
176 }
177 go s.Serve(grpcLis)
178 grpcListenAddr := grpcLis.Addr().(*net.TCPAddr)
179
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000180 registryServer := registry.NewServer()
181 registryServer.AddImage("testos", "latest", image)
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000182
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000183 registryLis, err := net.Listen("tcp", "127.0.0.1:0")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200184 if err != nil {
185 t.Fatal(err)
186 }
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200187
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000188 registryListenAddr := registryLis.Addr().(*net.TCPAddr)
189 go http.Serve(registryLis, registryServer)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200190
191 _, privateKey, err := ed25519.GenerateKey(rand.Reader)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200192 if err != nil {
193 t.Fatal(err)
194 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200195
196 init := apb.AgentInit{
197 TakeoverInit: &apb.TakeoverInit{
198 MachineId: "testbox1",
199 BmaasEndpoint: grpcAddr.String(),
200 CaCertificate: caCertRaw,
201 },
202 PrivateKey: privateKey,
203 }
204
205 rootDisk, err := os.CreateTemp("", "rootdisk")
206 if err != nil {
207 t.Fatal(err)
208 }
209 defer os.Remove(rootDisk.Name())
Lorenz Brun35fcf032023-06-29 04:15:58 +0200210 // Create a 10GiB sparse root disk
211 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200212 t.Fatalf("ftruncate failed: %v", err)
213 }
214
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000215 initramfsOrigFile, err := os.Open(xInitramfsOrigPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200216 if err != nil {
217 t.Fatal(err)
218 }
219 defer initramfsOrigFile.Close()
220
221 initramfsFile, err := os.CreateTemp("", "agent-initramfs")
222 if err != nil {
223 t.Fatal(err)
224 }
225 defer os.Remove(initramfsFile.Name())
226 if _, err := initramfsFile.ReadFrom(initramfsOrigFile); err != nil {
227 t.Fatal(err)
228 }
229
230 // Append AgentInit spec to initramfs
231 agentInitRaw, err := proto.Marshal(&init)
232 if err != nil {
233 t.Fatal(err)
234 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100235 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
236 if err != nil {
237 t.Fatal(err)
238 }
239 cpioW := cpio.NewWriter(compressedW)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200240 cpioW.WriteHeader(&cpio.Header{
241 Name: "/init.pb",
242 Size: int64(len(agentInitRaw)),
243 Mode: cpio.TypeReg | 0o644,
244 })
245 cpioW.Write(agentInitRaw)
246 cpioW.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +0100247 compressedW.Close()
Lorenz Brunaadeb792023-03-27 15:53:56 +0200248
249 grpcGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", grpcAddr.String(), grpcListenAddr.Port)
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000250 registryGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", registryAddr.String(), registryListenAddr.Port)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200251
252 ovmfVars, err := os.CreateTemp("", "agent-ovmf-vars")
253 if err != nil {
254 t.Fatal(err)
255 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000256 ovmfVarsTmpl, err := os.Open(xOvmfVarsPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200257 if err != nil {
258 t.Fatal(err)
259 }
260 if _, err := io.Copy(ovmfVars, ovmfVarsTmpl); err != nil {
261 t.Fatal(err)
262 }
263
264 qemuArgs := []string{
265 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
266 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000267 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200268 "-drive", "if=pflash,format=raw,file=" + ovmfVars.Name(),
269 "-drive", "if=virtio,format=raw,cache=unsafe,file=" + rootDisk.Name(),
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000270 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,%s,%s", grpcGuestFwd, registryGuestFwd),
Lorenz Brunaadeb792023-03-27 15:53:56 +0200271 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
272 "-device", "virtio-rng-pci",
273 "-serial", "stdio",
274 "-no-reboot",
275 }
276 stage1Args := append(qemuArgs,
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000277 "-kernel", xKernelPath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200278 "-initrd", initramfsFile.Name(),
279 "-append", "console=ttyS0 quiet")
280 qemuCmdAgent := exec.Command("qemu-system-x86_64", stage1Args...)
281 qemuCmdAgent.Stdout = os.Stdout
282 qemuCmdAgent.Stderr = os.Stderr
283 qemuCmdAgent.Run()
284 qemuCmdLaunch := exec.Command("qemu-system-x86_64", qemuArgs...)
285 stdoutPipe, err := qemuCmdLaunch.StdoutPipe()
286 if err != nil {
287 t.Fatal(err)
288 }
289 testosStarted := make(chan struct{})
290 go func() {
291 s := bufio.NewScanner(stdoutPipe)
292 for s.Scan() {
293 if strings.HasPrefix(s.Text(), "[") {
294 continue
295 }
296 t.Log("vm: " + s.Text())
297 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
298 testosStarted <- struct{}{}
299 break
300 }
301 }
302 qemuCmdLaunch.Wait()
303 }()
304 if err := qemuCmdLaunch.Start(); err != nil {
305 t.Fatal(err)
306 }
307 defer qemuCmdLaunch.Process.Kill()
308 select {
309 case <-testosStarted:
310 // Done, test passed
Jan Schär0af4dab2024-04-11 15:04:12 +0200311 case <-time.After(30 * time.Second):
Lorenz Brunaadeb792023-03-27 15:53:56 +0200312 t.Fatal("Waiting for TestOS launch timed out")
313 }
314}