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