blob: 9dfd129b87721398da7d21f9276f5ca3088d6748 [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
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020050 xQEMUPath string
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000051)
52
53func init() {
54 var err error
55 for _, path := range []*string{
Jan Schär4cc3d4d2025-04-14 11:46:47 +000056 &xImagePath, &xOvmfVarsPath, &xOvmfCodePath,
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020057 &xKernelPath, &xInitramfsOrigPath, &xQEMUPath,
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000058 } {
59 *path, err = runfiles.Rlocation(*path)
60 if err != nil {
61 panic(err)
62 }
63 }
64}
65
Lorenz Brunaadeb792023-03-27 15:53:56 +020066type fakeServer struct {
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +020067 hardwareReport *apb.AgentHardwareReport
68 installationRequest *apb.OSInstallationRequest
69 installationReport *apb.OSInstallationReport
Lorenz Brunaadeb792023-03-27 15:53:56 +020070}
71
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +020072func (f *fakeServer) Heartbeat(ctx context.Context, req *apb.HeartbeatRequest) (*apb.HeartbeatResponse, error) {
73 var res apb.HeartbeatResponse
Lorenz Brunaadeb792023-03-27 15:53:56 +020074 if req.HardwareReport != nil {
75 f.hardwareReport = req.HardwareReport
76 }
77 if req.InstallationReport != nil {
78 f.installationReport = req.InstallationReport
79 }
80 if f.installationRequest != nil {
81 res.InstallationRequest = f.installationRequest
82 }
83 return &res, nil
84}
85
86const GiB = 1024 * 1024 * 1024
87
88// TestMetropolisInstallE2E exercises the agent communicating against a test cloud
89// API server. This server requests the installation of the Metropolis 'TestOS',
90// which we then validate by looking for a string it outputs on boot.
91func TestMetropolisInstallE2E(t *testing.T) {
92 var f fakeServer
93
94 // Address inside fake QEMU userspace networking
95 grpcAddr := net.TCPAddr{
96 IP: net.IPv4(10, 42, 0, 5),
97 Port: 3000,
98 }
99
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000100 registryAddr := net.TCPAddr{
Lorenz Brunaadeb792023-03-27 15:53:56 +0200101 IP: net.IPv4(10, 42, 0, 6),
102 Port: 80,
103 }
104
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000105 image, err := oci.ReadLayout(xImagePath)
106 if err != nil {
107 t.Fatal(err)
108 }
109
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +0200110 f.installationRequest = &apb.OSInstallationRequest{
Lorenz Brunaadeb792023-03-27 15:53:56 +0200111 Generation: 5,
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +0200112 Type: &apb.OSInstallationRequest_Metropolis{Metropolis: &apb.MetropolisInstallationRequest{
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000113 OsImage: &mpb.OSImageRef{
114 Scheme: "http",
115 Host: registryAddr.String(),
116 Repository: "testos",
117 Tag: "latest",
Jan Schär2963b682025-07-17 17:03:44 +0200118 Digest: image.Digest(),
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000119 },
Lorenz Brunaadeb792023-03-27 15:53:56 +0200120 NodeParameters: &mpb.NodeParameters{},
121 RootDevice: "vda",
122 }},
123 }
124
125 caPubKey, caPrivKey, err := ed25519.GenerateKey(rand.Reader)
126 if err != nil {
127 t.Fatal(err)
128 }
129
130 caCertTmpl := x509.Certificate{
131 SerialNumber: big.NewInt(1),
132 Subject: pkix.Name{
133 CommonName: "Agent E2E Test CA",
134 },
135 NotBefore: time.Now(),
136 NotAfter: pki.UnknownNotAfter,
137 IsCA: true,
138 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
139 BasicConstraintsValid: true,
140 }
141 caCertRaw, err := x509.CreateCertificate(rand.Reader, &caCertTmpl, &caCertTmpl, caPubKey, caPrivKey)
142 if err != nil {
143 t.Fatal(err)
144 }
145 caCert, err := x509.ParseCertificate(caCertRaw)
146 if err != nil {
147 t.Fatal(err)
148 }
149
150 serverPubKey, serverPrivKey, err := ed25519.GenerateKey(rand.Reader)
151 if err != nil {
152 t.Fatal(err)
153 }
154 serverCertTmpl := x509.Certificate{
155 SerialNumber: big.NewInt(1),
156 Subject: pkix.Name{},
157 NotBefore: time.Now(),
158 NotAfter: pki.UnknownNotAfter,
159 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
160 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
161 IPAddresses: []net.IP{grpcAddr.IP},
162 BasicConstraintsValid: true,
163 }
164 serverCert, err := x509.CreateCertificate(rand.Reader, &serverCertTmpl, caCert, serverPubKey, caPrivKey)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200165 if err != nil {
166 t.Fatal(err)
167 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200168
169 s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&tls.Certificate{
170 Certificate: [][]byte{serverCert},
171 PrivateKey: serverPrivKey,
172 })))
Tim Windelschmidtb21bdf92025-05-28 18:37:35 +0200173 apb.RegisterAgentCallbackServer(s, &f)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200174 grpcLis, err := net.Listen("tcp", "127.0.0.1:0")
175 if err != nil {
176 panic(err)
177 }
178 go s.Serve(grpcLis)
179 grpcListenAddr := grpcLis.Addr().(*net.TCPAddr)
180
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000181 registryServer := registry.NewServer()
Jan Schär2963b682025-07-17 17:03:44 +0200182 registryServer.AddRef("testos", "latest", image)
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000183
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000184 registryLis, err := net.Listen("tcp", "127.0.0.1:0")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200185 if err != nil {
186 t.Fatal(err)
187 }
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200188
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000189 registryListenAddr := registryLis.Addr().(*net.TCPAddr)
190 go http.Serve(registryLis, registryServer)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200191
192 _, privateKey, err := ed25519.GenerateKey(rand.Reader)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200193 if err != nil {
194 t.Fatal(err)
195 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200196
197 init := apb.AgentInit{
198 TakeoverInit: &apb.TakeoverInit{
199 MachineId: "testbox1",
200 BmaasEndpoint: grpcAddr.String(),
201 CaCertificate: caCertRaw,
202 },
203 PrivateKey: privateKey,
204 }
205
206 rootDisk, err := os.CreateTemp("", "rootdisk")
207 if err != nil {
208 t.Fatal(err)
209 }
210 defer os.Remove(rootDisk.Name())
Lorenz Brun35fcf032023-06-29 04:15:58 +0200211 // Create a 10GiB sparse root disk
212 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200213 t.Fatalf("ftruncate failed: %v", err)
214 }
215
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000216 initramfsOrigFile, err := os.Open(xInitramfsOrigPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200217 if err != nil {
218 t.Fatal(err)
219 }
220 defer initramfsOrigFile.Close()
221
222 initramfsFile, err := os.CreateTemp("", "agent-initramfs")
223 if err != nil {
224 t.Fatal(err)
225 }
226 defer os.Remove(initramfsFile.Name())
227 if _, err := initramfsFile.ReadFrom(initramfsOrigFile); err != nil {
228 t.Fatal(err)
229 }
230
231 // Append AgentInit spec to initramfs
232 agentInitRaw, err := proto.Marshal(&init)
233 if err != nil {
234 t.Fatal(err)
235 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100236 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
237 if err != nil {
238 t.Fatal(err)
239 }
240 cpioW := cpio.NewWriter(compressedW)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200241 cpioW.WriteHeader(&cpio.Header{
242 Name: "/init.pb",
243 Size: int64(len(agentInitRaw)),
244 Mode: cpio.TypeReg | 0o644,
245 })
246 cpioW.Write(agentInitRaw)
247 cpioW.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +0100248 compressedW.Close()
Lorenz Brunaadeb792023-03-27 15:53:56 +0200249
250 grpcGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", grpcAddr.String(), grpcListenAddr.Port)
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000251 registryGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", registryAddr.String(), registryListenAddr.Port)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200252
253 ovmfVars, err := os.CreateTemp("", "agent-ovmf-vars")
254 if err != nil {
255 t.Fatal(err)
256 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000257 ovmfVarsTmpl, err := os.Open(xOvmfVarsPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200258 if err != nil {
259 t.Fatal(err)
260 }
261 if _, err := io.Copy(ovmfVars, ovmfVarsTmpl); err != nil {
262 t.Fatal(err)
263 }
264
265 qemuArgs := []string{
266 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
267 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000268 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200269 "-drive", "if=pflash,format=raw,file=" + ovmfVars.Name(),
270 "-drive", "if=virtio,format=raw,cache=unsafe,file=" + rootDisk.Name(),
Jan Schär4cc3d4d2025-04-14 11:46:47 +0000271 "-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 +0200272 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
273 "-device", "virtio-rng-pci",
274 "-serial", "stdio",
275 "-no-reboot",
276 }
277 stage1Args := append(qemuArgs,
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000278 "-kernel", xKernelPath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200279 "-initrd", initramfsFile.Name(),
280 "-append", "console=ttyS0 quiet")
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +0200281 qemuCmdAgent := exec.Command(xQEMUPath, stage1Args...)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200282 qemuCmdAgent.Stdout = os.Stdout
283 qemuCmdAgent.Stderr = os.Stderr
284 qemuCmdAgent.Run()
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +0200285
286 qemuCmdLaunch := exec.Command(xQEMUPath, qemuArgs...)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200287 stdoutPipe, err := qemuCmdLaunch.StdoutPipe()
288 if err != nil {
289 t.Fatal(err)
290 }
291 testosStarted := make(chan struct{})
292 go func() {
293 s := bufio.NewScanner(stdoutPipe)
294 for s.Scan() {
295 if strings.HasPrefix(s.Text(), "[") {
296 continue
297 }
298 t.Log("vm: " + s.Text())
299 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
300 testosStarted <- struct{}{}
301 break
302 }
303 }
304 qemuCmdLaunch.Wait()
305 }()
306 if err := qemuCmdLaunch.Start(); err != nil {
307 t.Fatal(err)
308 }
309 defer qemuCmdLaunch.Process.Kill()
310 select {
311 case <-testosStarted:
312 // Done, test passed
Jan Schär0af4dab2024-04-11 15:04:12 +0200313 case <-time.After(30 * time.Second):
Lorenz Brunaadeb792023-03-27 15:53:56 +0200314 t.Fatal("Waiting for TestOS launch timed out")
315 }
316}