blob: dcde625b6d389520a20939a945be745551aa7567 [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"
Jan Schär0af4dab2024-04-11 15:04:12 +020033 mpb "source.monogon.dev/metropolis/proto/api"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020034 "source.monogon.dev/osbase/pki"
Lorenz Brunaadeb792023-03-27 15:53:56 +020035)
36
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000037var (
38 // These are filled by bazel at linking time with the canonical path of
39 // their corresponding file. Inside the init function we resolve it
40 // with the rules_go runfiles package to the real path.
41 xBundleFilePath string
42 xOvmfVarsPath string
43 xOvmfCodePath string
44 xKernelPath string
45 xInitramfsOrigPath string
46)
47
48func init() {
49 var err error
50 for _, path := range []*string{
51 &xBundleFilePath, &xOvmfVarsPath, &xOvmfCodePath,
52 &xKernelPath, &xInitramfsOrigPath,
53 } {
54 *path, err = runfiles.Rlocation(*path)
55 if err != nil {
56 panic(err)
57 }
58 }
59}
60
Lorenz Brunaadeb792023-03-27 15:53:56 +020061type fakeServer struct {
62 hardwareReport *bpb.AgentHardwareReport
63 installationRequest *bpb.OSInstallationRequest
64 installationReport *bpb.OSInstallationReport
65}
66
67func (f *fakeServer) Heartbeat(ctx context.Context, req *bpb.AgentHeartbeatRequest) (*bpb.AgentHeartbeatResponse, error) {
68 var res bpb.AgentHeartbeatResponse
69 if req.HardwareReport != nil {
70 f.hardwareReport = req.HardwareReport
71 }
72 if req.InstallationReport != nil {
73 f.installationReport = req.InstallationReport
74 }
75 if f.installationRequest != nil {
76 res.InstallationRequest = f.installationRequest
77 }
78 return &res, nil
79}
80
81const GiB = 1024 * 1024 * 1024
82
83// TestMetropolisInstallE2E exercises the agent communicating against a test cloud
84// API server. This server requests the installation of the Metropolis 'TestOS',
85// which we then validate by looking for a string it outputs on boot.
86func TestMetropolisInstallE2E(t *testing.T) {
87 var f fakeServer
88
89 // Address inside fake QEMU userspace networking
90 grpcAddr := net.TCPAddr{
91 IP: net.IPv4(10, 42, 0, 5),
92 Port: 3000,
93 }
94
95 blobAddr := net.TCPAddr{
96 IP: net.IPv4(10, 42, 0, 6),
97 Port: 80,
98 }
99
100 f.installationRequest = &bpb.OSInstallationRequest{
101 Generation: 5,
102 Type: &bpb.OSInstallationRequest_Metropolis{Metropolis: &bpb.MetropolisInstallationRequest{
103 BundleUrl: (&url.URL{Scheme: "http", Host: blobAddr.String(), Path: "/bundle.bin"}).String(),
104 NodeParameters: &mpb.NodeParameters{},
105 RootDevice: "vda",
106 }},
107 }
108
109 caPubKey, caPrivKey, err := ed25519.GenerateKey(rand.Reader)
110 if err != nil {
111 t.Fatal(err)
112 }
113
114 caCertTmpl := x509.Certificate{
115 SerialNumber: big.NewInt(1),
116 Subject: pkix.Name{
117 CommonName: "Agent E2E Test CA",
118 },
119 NotBefore: time.Now(),
120 NotAfter: pki.UnknownNotAfter,
121 IsCA: true,
122 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
123 BasicConstraintsValid: true,
124 }
125 caCertRaw, err := x509.CreateCertificate(rand.Reader, &caCertTmpl, &caCertTmpl, caPubKey, caPrivKey)
126 if err != nil {
127 t.Fatal(err)
128 }
129 caCert, err := x509.ParseCertificate(caCertRaw)
130 if err != nil {
131 t.Fatal(err)
132 }
133
134 serverPubKey, serverPrivKey, err := ed25519.GenerateKey(rand.Reader)
135 if err != nil {
136 t.Fatal(err)
137 }
138 serverCertTmpl := x509.Certificate{
139 SerialNumber: big.NewInt(1),
140 Subject: pkix.Name{},
141 NotBefore: time.Now(),
142 NotAfter: pki.UnknownNotAfter,
143 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
144 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
145 IPAddresses: []net.IP{grpcAddr.IP},
146 BasicConstraintsValid: true,
147 }
148 serverCert, err := x509.CreateCertificate(rand.Reader, &serverCertTmpl, caCert, serverPubKey, caPrivKey)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200149 if err != nil {
150 t.Fatal(err)
151 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200152
153 s := grpc.NewServer(grpc.Creds(credentials.NewServerTLSFromCert(&tls.Certificate{
154 Certificate: [][]byte{serverCert},
155 PrivateKey: serverPrivKey,
156 })))
157 bpb.RegisterAgentCallbackServer(s, &f)
158 grpcLis, err := net.Listen("tcp", "127.0.0.1:0")
159 if err != nil {
160 panic(err)
161 }
162 go s.Serve(grpcLis)
163 grpcListenAddr := grpcLis.Addr().(*net.TCPAddr)
164
165 m := http.NewServeMux()
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000166
Lorenz Brunaadeb792023-03-27 15:53:56 +0200167 m.HandleFunc("/bundle.bin", func(w http.ResponseWriter, req *http.Request) {
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000168 http.ServeFile(w, req, xBundleFilePath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200169 })
170 blobLis, err := net.Listen("tcp", "127.0.0.1:0")
171 if err != nil {
172 t.Fatal(err)
173 }
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200174
Lorenz Brunaadeb792023-03-27 15:53:56 +0200175 blobListenAddr := blobLis.Addr().(*net.TCPAddr)
176 go http.Serve(blobLis, m)
177
178 _, privateKey, err := ed25519.GenerateKey(rand.Reader)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200179 if err != nil {
180 t.Fatal(err)
181 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200182
183 init := apb.AgentInit{
184 TakeoverInit: &apb.TakeoverInit{
185 MachineId: "testbox1",
186 BmaasEndpoint: grpcAddr.String(),
187 CaCertificate: caCertRaw,
188 },
189 PrivateKey: privateKey,
190 }
191
192 rootDisk, err := os.CreateTemp("", "rootdisk")
193 if err != nil {
194 t.Fatal(err)
195 }
196 defer os.Remove(rootDisk.Name())
Lorenz Brun35fcf032023-06-29 04:15:58 +0200197 // Create a 10GiB sparse root disk
198 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200199 t.Fatalf("ftruncate failed: %v", err)
200 }
201
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000202 initramfsOrigFile, err := os.Open(xInitramfsOrigPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200203 if err != nil {
204 t.Fatal(err)
205 }
206 defer initramfsOrigFile.Close()
207
208 initramfsFile, err := os.CreateTemp("", "agent-initramfs")
209 if err != nil {
210 t.Fatal(err)
211 }
212 defer os.Remove(initramfsFile.Name())
213 if _, err := initramfsFile.ReadFrom(initramfsOrigFile); err != nil {
214 t.Fatal(err)
215 }
216
217 // Append AgentInit spec to initramfs
218 agentInitRaw, err := proto.Marshal(&init)
219 if err != nil {
220 t.Fatal(err)
221 }
Lorenz Brun62f1d362023-11-14 16:18:24 +0100222 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
223 if err != nil {
224 t.Fatal(err)
225 }
226 cpioW := cpio.NewWriter(compressedW)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200227 cpioW.WriteHeader(&cpio.Header{
228 Name: "/init.pb",
229 Size: int64(len(agentInitRaw)),
230 Mode: cpio.TypeReg | 0o644,
231 })
232 cpioW.Write(agentInitRaw)
233 cpioW.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +0100234 compressedW.Close()
Lorenz Brunaadeb792023-03-27 15:53:56 +0200235
236 grpcGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", grpcAddr.String(), grpcListenAddr.Port)
237 blobGuestFwd := fmt.Sprintf("guestfwd=tcp:%s-tcp:127.0.0.1:%d", blobAddr.String(), blobListenAddr.Port)
238
239 ovmfVars, err := os.CreateTemp("", "agent-ovmf-vars")
240 if err != nil {
241 t.Fatal(err)
242 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000243 ovmfVarsTmpl, err := os.Open(xOvmfVarsPath)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200244 if err != nil {
245 t.Fatal(err)
246 }
247 if _, err := io.Copy(ovmfVars, ovmfVarsTmpl); err != nil {
248 t.Fatal(err)
249 }
250
251 qemuArgs := []string{
252 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
253 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000254 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200255 "-drive", "if=pflash,format=raw,file=" + ovmfVars.Name(),
256 "-drive", "if=virtio,format=raw,cache=unsafe,file=" + rootDisk.Name(),
257 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,%s,%s", grpcGuestFwd, blobGuestFwd),
258 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
259 "-device", "virtio-rng-pci",
260 "-serial", "stdio",
261 "-no-reboot",
262 }
263 stage1Args := append(qemuArgs,
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000264 "-kernel", xKernelPath,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200265 "-initrd", initramfsFile.Name(),
266 "-append", "console=ttyS0 quiet")
267 qemuCmdAgent := exec.Command("qemu-system-x86_64", stage1Args...)
268 qemuCmdAgent.Stdout = os.Stdout
269 qemuCmdAgent.Stderr = os.Stderr
270 qemuCmdAgent.Run()
271 qemuCmdLaunch := exec.Command("qemu-system-x86_64", qemuArgs...)
272 stdoutPipe, err := qemuCmdLaunch.StdoutPipe()
273 if err != nil {
274 t.Fatal(err)
275 }
276 testosStarted := make(chan struct{})
277 go func() {
278 s := bufio.NewScanner(stdoutPipe)
279 for s.Scan() {
280 if strings.HasPrefix(s.Text(), "[") {
281 continue
282 }
283 t.Log("vm: " + s.Text())
284 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
285 testosStarted <- struct{}{}
286 break
287 }
288 }
289 qemuCmdLaunch.Wait()
290 }()
291 if err := qemuCmdLaunch.Start(); err != nil {
292 t.Fatal(err)
293 }
294 defer qemuCmdLaunch.Process.Kill()
295 select {
296 case <-testosStarted:
297 // Done, test passed
Jan Schär0af4dab2024-04-11 15:04:12 +0200298 case <-time.After(30 * time.Second):
Lorenz Brunaadeb792023-03-27 15:53:56 +0200299 t.Fatal("Waiting for TestOS launch timed out")
300 }
301}