blob: 100553ebb9dfc69eff14733a39aac77f40b30db7 [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"
24 "github.com/pierrec/lz4/v4"
25 "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())
169 // Create a 5GiB sparse root disk
170 if err := unix.Ftruncate(int(rootDisk.Fd()), 5*GiB); err != nil {
171 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 }
186 initramfsOrigPath, err := datafile.ResolveRunfile("cloud/agent/initramfs.cpio.lz4")
187 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 }
210 compressedOut := lz4.NewWriter(initramfsFile)
211 compressedOut.Apply(lz4.LegacyOption(true))
212 cpioW := cpio.NewWriter(compressedOut)
213 cpioW.WriteHeader(&cpio.Header{
214 Name: "/init.pb",
215 Size: int64(len(agentInitRaw)),
216 Mode: cpio.TypeReg | 0o644,
217 })
218 cpioW.Write(agentInitRaw)
219 cpioW.Close()
220 compressedOut.Close()
221
222 grpcGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", grpcAddr.String(), grpcListenAddr.Port)
223 blobGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", blobAddr.String(), blobListenAddr.Port)
224
225 ovmfVars, err := os.CreateTemp("", "agent-ovmf-vars")
226 if err != nil {
227 t.Fatal(err)
228 }
229 ovmfVarsTmpl, err := os.Open(ovmfVarsPath)
230 if err != nil {
231 t.Fatal(err)
232 }
233 if _, err := io.Copy(ovmfVars, ovmfVarsTmpl); err != nil {
234 t.Fatal(err)
235 }
236
237 qemuArgs := []string{
238 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
239 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
240 "-drive", "if=pflash,format=raw,readonly=on,file=" + ovmfCodePath,
241 "-drive", "if=pflash,format=raw,file=" + ovmfVars.Name(),
242 "-drive", "if=virtio,format=raw,cache=unsafe,file=" + rootDisk.Name(),
243 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,%s,%s", grpcGuestFwd, blobGuestFwd),
244 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
245 "-device", "virtio-rng-pci",
246 "-serial", "stdio",
247 "-no-reboot",
248 }
249 stage1Args := append(qemuArgs,
250 "-kernel", kernelPath,
251 "-initrd", initramfsFile.Name(),
252 "-append", "console=ttyS0 quiet")
253 qemuCmdAgent := exec.Command("qemu-system-x86_64", stage1Args...)
254 qemuCmdAgent.Stdout = os.Stdout
255 qemuCmdAgent.Stderr = os.Stderr
256 qemuCmdAgent.Run()
257 qemuCmdLaunch := exec.Command("qemu-system-x86_64", qemuArgs...)
258 stdoutPipe, err := qemuCmdLaunch.StdoutPipe()
259 if err != nil {
260 t.Fatal(err)
261 }
262 testosStarted := make(chan struct{})
263 go func() {
264 s := bufio.NewScanner(stdoutPipe)
265 for s.Scan() {
266 if strings.HasPrefix(s.Text(), "[") {
267 continue
268 }
269 t.Log("vm: " + s.Text())
270 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
271 testosStarted <- struct{}{}
272 break
273 }
274 }
275 qemuCmdLaunch.Wait()
276 }()
277 if err := qemuCmdLaunch.Start(); err != nil {
278 t.Fatal(err)
279 }
280 defer qemuCmdLaunch.Process.Kill()
281 select {
282 case <-testosStarted:
283 // Done, test passed
284 case <-time.After(10 * time.Second):
285 t.Fatal("Waiting for TestOS launch timed out")
286 }
287}