blob: 277cc5535fd19a0d9ce9cf73b4c7ba01affe21f4 [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
23 "github.com/cavaliergopher/cpio"
Lorenz Brun62f1d362023-11-14 16:18:24 +010024 "github.com/klauspost/compress/zstd"
Lorenz Brunaadeb792023-03-27 15:53:56 +020025 "golang.org/x/sys/unix"
26 "google.golang.org/grpc"
27 "google.golang.org/grpc/credentials"
28 "google.golang.org/protobuf/proto"
29
30 apb "source.monogon.dev/cloud/agent/api"
31 bpb "source.monogon.dev/cloud/bmaas/server/api"
32 "source.monogon.dev/metropolis/cli/pkg/datafile"
33 "source.monogon.dev/metropolis/pkg/pki"
34 mpb "source.monogon.dev/metropolis/proto/api"
35)
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)
125
126 s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&tls.Certificate{
127 Certificate: [][]byte{serverCert},
128 PrivateKey: serverPrivKey,
129 })))
130 bpb.RegisterAgentCallbackServer(s, &f)
131 grpcLis, err := net.Listen("tcp", "127.0.0.1:0")
132 if err != nil {
133 panic(err)
134 }
135 go s.Serve(grpcLis)
136 grpcListenAddr := grpcLis.Addr().(*net.TCPAddr)
137
138 m := http.NewServeMux()
139 bundleFilePath, err := datafile.ResolveRunfile("metropolis/installer/test/testos/testos_bundle.zip")
140 if err != nil {
141 t.Fatal(err)
142 }
143 m.HandleFunc("/bundle.bin", func(w http.ResponseWriter, req *http.Request) {
144 http.ServeFile(w, req, bundleFilePath)
145 })
146 blobLis, err := net.Listen("tcp", "127.0.0.1:0")
147 if err != nil {
148 t.Fatal(err)
149 }
150 blobListenAddr := blobLis.Addr().(*net.TCPAddr)
151 go http.Serve(blobLis, m)
152
153 _, privateKey, err := ed25519.GenerateKey(rand.Reader)
154
155 init := apb.AgentInit{
156 TakeoverInit: &apb.TakeoverInit{
157 MachineId: "testbox1",
158 BmaasEndpoint: grpcAddr.String(),
159 CaCertificate: caCertRaw,
160 },
161 PrivateKey: privateKey,
162 }
163
164 rootDisk, err := os.CreateTemp("", "rootdisk")
165 if err != nil {
166 t.Fatal(err)
167 }
168 defer os.Remove(rootDisk.Name())
Lorenz Brun35fcf032023-06-29 04:15:58 +0200169 // Create a 10GiB sparse root disk
170 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200171 t.Fatalf("ftruncate failed: %v", err)
172 }
173
174 ovmfVarsPath, err := datafile.ResolveRunfile("external/edk2/OVMF_VARS.fd")
175 if err != nil {
176 t.Fatal(err)
177 }
178 ovmfCodePath, err := datafile.ResolveRunfile("external/edk2/OVMF_CODE.fd")
179 if err != nil {
180 t.Fatal(err)
181 }
182 kernelPath, err := datafile.ResolveRunfile("third_party/linux/bzImage")
183 if err != nil {
184 t.Fatal(err)
185 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100186 initramfsOrigPath, err := datafile.ResolveRunfile("cloud/agent/initramfs.cpio.zst")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200187 if err != nil {
188 t.Fatal(err)
189 }
190 initramfsOrigFile, err := os.Open(initramfsOrigPath)
191 if err != nil {
192 t.Fatal(err)
193 }
194 defer initramfsOrigFile.Close()
195
196 initramfsFile, err := os.CreateTemp("", "agent-initramfs")
197 if err != nil {
198 t.Fatal(err)
199 }
200 defer os.Remove(initramfsFile.Name())
201 if _, err := initramfsFile.ReadFrom(initramfsOrigFile); err != nil {
202 t.Fatal(err)
203 }
204
205 // Append AgentInit spec to initramfs
206 agentInitRaw, err := proto.Marshal(&init)
207 if err != nil {
208 t.Fatal(err)
209 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100210 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
211 if err != nil {
212 t.Fatal(err)
213 }
214 cpioW := cpio.NewWriter(compressedW)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200215 cpioW.WriteHeader(&cpio.Header{
216 Name: "/init.pb",
217 Size: int64(len(agentInitRaw)),
218 Mode: cpio.TypeReg | 0o644,
219 })
220 cpioW.Write(agentInitRaw)
221 cpioW.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +0100222 compressedW.Close()
Lorenz Brunaadeb792023-03-27 15:53:56 +0200223
224 grpcGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", grpcAddr.String(), grpcListenAddr.Port)
225 blobGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", blobAddr.String(), blobListenAddr.Port)
226
227 ovmfVars, err := os.CreateTemp("", "agent-ovmf-vars")
228 if err != nil {
229 t.Fatal(err)
230 }
231 ovmfVarsTmpl, err := os.Open(ovmfVarsPath)
232 if err != nil {
233 t.Fatal(err)
234 }
235 if _, err := io.Copy(ovmfVars, ovmfVarsTmpl); err != nil {
236 t.Fatal(err)
237 }
238
239 qemuArgs := []string{
240 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
241 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
242 "-drive", "if=pflash,format=raw,readonly=on,file=" + ovmfCodePath,
243 "-drive", "if=pflash,format=raw,file=" + ovmfVars.Name(),
244 "-drive", "if=virtio,format=raw,cache=unsafe,file=" + rootDisk.Name(),
245 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,%s,%s", grpcGuestFwd, blobGuestFwd),
246 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
247 "-device", "virtio-rng-pci",
248 "-serial", "stdio",
249 "-no-reboot",
250 }
251 stage1Args := append(qemuArgs,
252 "-kernel", kernelPath,
253 "-initrd", initramfsFile.Name(),
254 "-append", "console=ttyS0 quiet")
255 qemuCmdAgent := exec.Command("qemu-system-x86_64", stage1Args...)
256 qemuCmdAgent.Stdout = os.Stdout
257 qemuCmdAgent.Stderr = os.Stderr
258 qemuCmdAgent.Run()
259 qemuCmdLaunch := exec.Command("qemu-system-x86_64", qemuArgs...)
260 stdoutPipe, err := qemuCmdLaunch.StdoutPipe()
261 if err != nil {
262 t.Fatal(err)
263 }
264 testosStarted := make(chan struct{})
265 go func() {
266 s := bufio.NewScanner(stdoutPipe)
267 for s.Scan() {
268 if strings.HasPrefix(s.Text(), "[") {
269 continue
270 }
271 t.Log("vm: " + s.Text())
272 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
273 testosStarted <- struct{}{}
274 break
275 }
276 }
277 qemuCmdLaunch.Wait()
278 }()
279 if err := qemuCmdLaunch.Start(); err != nil {
280 t.Fatal(err)
281 }
282 defer qemuCmdLaunch.Process.Kill()
283 select {
284 case <-testosStarted:
285 // Done, test passed
286 case <-time.After(10 * time.Second):
287 t.Fatal("Waiting for TestOS launch timed out")
288 }
289}