blob: 4fd06ecc6d43bbe44c47fb70bc74b432ed11dd2b [file] [log] [blame]
Lorenz Brunaadeb792023-03-27 15:53:56 +02001package e2e
2
3import (
4 "bufio"
5 "context"
6 "crypto/ed25519"
7 "crypto/rand"
8 "crypto/tls"
9 "crypto/x509"
10 "crypto/x509/pkix"
11 "fmt"
12 "io"
13 "math/big"
14 "net"
15 "net/http"
16 "net/url"
17 "os"
18 "os/exec"
19 "strings"
20 "testing"
21 "time"
22
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010023 "github.com/bazelbuild/rules_go/go/runfiles"
Lorenz Brunaadeb792023-03-27 15:53:56 +020024 "github.com/cavaliergopher/cpio"
Lorenz Brun62f1d362023-11-14 16:18:24 +010025 "github.com/klauspost/compress/zstd"
Lorenz Brunaadeb792023-03-27 15:53:56 +020026 "golang.org/x/sys/unix"
27 "google.golang.org/grpc"
28 "google.golang.org/grpc/credentials"
29 "google.golang.org/protobuf/proto"
30
31 apb "source.monogon.dev/cloud/agent/api"
32 bpb "source.monogon.dev/cloud/bmaas/server/api"
Lorenz Brunaadeb792023-03-27 15:53:56 +020033 mpb "source.monogon.dev/metropolis/proto/api"
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010034
35 "source.monogon.dev/metropolis/pkg/pki"
Lorenz Brunaadeb792023-03-27 15:53:56 +020036)
37
38type fakeServer struct {
39 hardwareReport *bpb.AgentHardwareReport
40 installationRequest *bpb.OSInstallationRequest
41 installationReport *bpb.OSInstallationReport
42}
43
44func (f *fakeServer) Heartbeat(ctx context.Context, req *bpb.AgentHeartbeatRequest) (*bpb.AgentHeartbeatResponse, error) {
45 var res bpb.AgentHeartbeatResponse
46 if req.HardwareReport != nil {
47 f.hardwareReport = req.HardwareReport
48 }
49 if req.InstallationReport != nil {
50 f.installationReport = req.InstallationReport
51 }
52 if f.installationRequest != nil {
53 res.InstallationRequest = f.installationRequest
54 }
55 return &res, nil
56}
57
58const GiB = 1024 * 1024 * 1024
59
60// TestMetropolisInstallE2E exercises the agent communicating against a test cloud
61// API server. This server requests the installation of the Metropolis 'TestOS',
62// which we then validate by looking for a string it outputs on boot.
63func TestMetropolisInstallE2E(t *testing.T) {
64 var f fakeServer
65
66 // Address inside fake QEMU userspace networking
67 grpcAddr := net.TCPAddr{
68 IP: net.IPv4(10, 42, 0, 5),
69 Port: 3000,
70 }
71
72 blobAddr := net.TCPAddr{
73 IP: net.IPv4(10, 42, 0, 6),
74 Port: 80,
75 }
76
77 f.installationRequest = &bpb.OSInstallationRequest{
78 Generation: 5,
79 Type: &bpb.OSInstallationRequest_Metropolis{Metropolis: &bpb.MetropolisInstallationRequest{
80 BundleUrl: (&url.URL{Scheme: "http", Host: blobAddr.String(), Path: "/bundle.bin"}).String(),
81 NodeParameters: &mpb.NodeParameters{},
82 RootDevice: "vda",
83 }},
84 }
85
86 caPubKey, caPrivKey, err := ed25519.GenerateKey(rand.Reader)
87 if err != nil {
88 t.Fatal(err)
89 }
90
91 caCertTmpl := x509.Certificate{
92 SerialNumber: big.NewInt(1),
93 Subject: pkix.Name{
94 CommonName: "Agent E2E Test CA",
95 },
96 NotBefore: time.Now(),
97 NotAfter: pki.UnknownNotAfter,
98 IsCA: true,
99 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
100 BasicConstraintsValid: true,
101 }
102 caCertRaw, err := x509.CreateCertificate(rand.Reader, &caCertTmpl, &caCertTmpl, caPubKey, caPrivKey)
103 if err != nil {
104 t.Fatal(err)
105 }
106 caCert, err := x509.ParseCertificate(caCertRaw)
107 if err != nil {
108 t.Fatal(err)
109 }
110
111 serverPubKey, serverPrivKey, err := ed25519.GenerateKey(rand.Reader)
112 if err != nil {
113 t.Fatal(err)
114 }
115 serverCertTmpl := x509.Certificate{
116 SerialNumber: big.NewInt(1),
117 Subject: pkix.Name{},
118 NotBefore: time.Now(),
119 NotAfter: pki.UnknownNotAfter,
120 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
121 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
122 IPAddresses: []net.IP{grpcAddr.IP},
123 BasicConstraintsValid: true,
124 }
125 serverCert, err := x509.CreateCertificate(rand.Reader, &serverCertTmpl, caCert, serverPubKey, caPrivKey)
126
127 s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&tls.Certificate{
128 Certificate: [][]byte{serverCert},
129 PrivateKey: serverPrivKey,
130 })))
131 bpb.RegisterAgentCallbackServer(s, &f)
132 grpcLis, err := net.Listen("tcp", "127.0.0.1:0")
133 if err != nil {
134 panic(err)
135 }
136 go s.Serve(grpcLis)
137 grpcListenAddr := grpcLis.Addr().(*net.TCPAddr)
138
139 m := http.NewServeMux()
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +0100140 bundleFilePath, err := runfiles.Rlocation("_main/metropolis/installer/test/testos/testos_bundle.zip")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200141 if err != nil {
142 t.Fatal(err)
143 }
144 m.HandleFunc("/bundle.bin", func(w http.ResponseWriter, req *http.Request) {
145 http.ServeFile(w, req, bundleFilePath)
146 })
147 blobLis, err := net.Listen("tcp", "127.0.0.1:0")
148 if err != nil {
149 t.Fatal(err)
150 }
151 blobListenAddr := blobLis.Addr().(*net.TCPAddr)
152 go http.Serve(blobLis, m)
153
154 _, privateKey, err := ed25519.GenerateKey(rand.Reader)
155
156 init := apb.AgentInit{
157 TakeoverInit: &apb.TakeoverInit{
158 MachineId: "testbox1",
159 BmaasEndpoint: grpcAddr.String(),
160 CaCertificate: caCertRaw,
161 },
162 PrivateKey: privateKey,
163 }
164
165 rootDisk, err := os.CreateTemp("", "rootdisk")
166 if err != nil {
167 t.Fatal(err)
168 }
169 defer os.Remove(rootDisk.Name())
Lorenz Brun35fcf032023-06-29 04:15:58 +0200170 // Create a 10GiB sparse root disk
171 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200172 t.Fatalf("ftruncate failed: %v", err)
173 }
174
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +0100175 ovmfVarsPath, err := runfiles.Rlocation("edk2/OVMF_VARS.fd")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200176 if err != nil {
177 t.Fatal(err)
178 }
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +0100179 ovmfCodePath, err := runfiles.Rlocation("edk2/OVMF_CODE.fd")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200180 if err != nil {
181 t.Fatal(err)
182 }
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +0100183 kernelPath, err := runfiles.Rlocation("_main/third_party/linux/bzImage")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200184 if err != nil {
185 t.Fatal(err)
186 }
Tim Windelschmidt79ffbbe2024-02-22 19:15:51 +0100187 initramfsOrigPath, err := runfiles.Rlocation("_main/cloud/agent/takeover/initramfs.cpio.zst")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200188 if err != nil {
189 t.Fatal(err)
190 }
191 initramfsOrigFile, err := os.Open(initramfsOrigPath)
192 if err != nil {
193 t.Fatal(err)
194 }
195 defer initramfsOrigFile.Close()
196
197 initramfsFile, err := os.CreateTemp("", "agent-initramfs")
198 if err != nil {
199 t.Fatal(err)
200 }
201 defer os.Remove(initramfsFile.Name())
202 if _, err := initramfsFile.ReadFrom(initramfsOrigFile); err != nil {
203 t.Fatal(err)
204 }
205
206 // Append AgentInit spec to initramfs
207 agentInitRaw, err := proto.Marshal(&init)
208 if err != nil {
209 t.Fatal(err)
210 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100211 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
212 if err != nil {
213 t.Fatal(err)
214 }
215 cpioW := cpio.NewWriter(compressedW)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200216 cpioW.WriteHeader(&cpio.Header{
217 Name: "/init.pb",
218 Size: int64(len(agentInitRaw)),
219 Mode: cpio.TypeReg | 0o644,
220 })
221 cpioW.Write(agentInitRaw)
222 cpioW.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +0100223 compressedW.Close()
Lorenz Brunaadeb792023-03-27 15:53:56 +0200224
225 grpcGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", grpcAddr.String(), grpcListenAddr.Port)
226 blobGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", blobAddr.String(), blobListenAddr.Port)
227
228 ovmfVars, err := os.CreateTemp("", "agent-ovmf-vars")
229 if err != nil {
230 t.Fatal(err)
231 }
232 ovmfVarsTmpl, err := os.Open(ovmfVarsPath)
233 if err != nil {
234 t.Fatal(err)
235 }
236 if _, err := io.Copy(ovmfVars, ovmfVarsTmpl); err != nil {
237 t.Fatal(err)
238 }
239
240 qemuArgs := []string{
241 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
242 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
243 "-drive", "if=pflash,format=raw,readonly=on,file=" + ovmfCodePath,
244 "-drive", "if=pflash,format=raw,file=" + ovmfVars.Name(),
245 "-drive", "if=virtio,format=raw,cache=unsafe,file=" + rootDisk.Name(),
246 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,%s,%s", grpcGuestFwd, blobGuestFwd),
247 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
248 "-device", "virtio-rng-pci",
249 "-serial", "stdio",
250 "-no-reboot",
251 }
252 stage1Args := append(qemuArgs,
253 "-kernel", kernelPath,
254 "-initrd", initramfsFile.Name(),
255 "-append", "console=ttyS0 quiet")
256 qemuCmdAgent := exec.Command("qemu-system-x86_64", stage1Args...)
257 qemuCmdAgent.Stdout = os.Stdout
258 qemuCmdAgent.Stderr = os.Stderr
259 qemuCmdAgent.Run()
260 qemuCmdLaunch := exec.Command("qemu-system-x86_64", qemuArgs...)
261 stdoutPipe, err := qemuCmdLaunch.StdoutPipe()
262 if err != nil {
263 t.Fatal(err)
264 }
265 testosStarted := make(chan struct{})
266 go func() {
267 s := bufio.NewScanner(stdoutPipe)
268 for s.Scan() {
269 if strings.HasPrefix(s.Text(), "[") {
270 continue
271 }
272 t.Log("vm: " + s.Text())
273 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
274 testosStarted <- struct{}{}
275 break
276 }
277 }
278 qemuCmdLaunch.Wait()
279 }()
280 if err := qemuCmdLaunch.Start(); err != nil {
281 t.Fatal(err)
282 }
283 defer qemuCmdLaunch.Process.Kill()
284 select {
285 case <-testosStarted:
286 // Done, test passed
287 case <-time.After(10 * time.Second):
288 t.Fatal("Waiting for TestOS launch timed out")
289 }
290}