blob: afce5156070834d427c28fe180c12be473f9ba3c [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +01004package e2e
5
6import (
7 "bufio"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +01008 "crypto/ed25519"
9 "crypto/rand"
10 "encoding/json"
Jan Schärec61a472025-03-24 18:54:00 +000011 "errors"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010012 "fmt"
Jan Schärec61a472025-03-24 18:54:00 +000013 "io"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010014 "net"
15 "os"
16 "os/exec"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010017 "strings"
18 "testing"
19 "time"
20
21 "github.com/bazelbuild/rules_go/go/runfiles"
22 xssh "golang.org/x/crypto/ssh"
Jan Schärec61a472025-03-24 18:54:00 +000023 "golang.org/x/crypto/ssh/agent"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010024 "golang.org/x/sys/unix"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010025
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010026 "source.monogon.dev/osbase/fat32"
27 "source.monogon.dev/osbase/freeport"
Jan Schärc1b6df42025-03-20 08:52:18 +000028 "source.monogon.dev/osbase/structfs"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010029)
30
31var (
32 // These are filled by bazel at linking time with the canonical path of
33 // their corresponding file. Inside the init function we resolve it
34 // with the rules_go runfiles package to the real path.
35 xBundleFilePath string
36 xOvmfVarsPath string
37 xOvmfCodePath string
38 xCloudImagePath string
39 xTakeoverPath string
Jan Schärec61a472025-03-24 18:54:00 +000040 xMetroctlPath string
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010041)
42
43func init() {
44 var err error
45 for _, path := range []*string{
46 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
Jan Schärec61a472025-03-24 18:54:00 +000047 &xTakeoverPath, &xBundleFilePath, &xMetroctlPath,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010048 } {
49 *path, err = runfiles.Rlocation(*path)
50 if err != nil {
51 panic(err)
52 }
53 }
54}
55
56const GiB = 1024 * 1024 * 1024
57
58func TestE2E(t *testing.T) {
59 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
60 if err != nil {
61 t.Fatal(err)
62 }
63
64 sshPubKey, err := xssh.NewPublicKey(pubKey)
65 if err != nil {
66 t.Fatal(err)
67 }
68
Jan Schärec61a472025-03-24 18:54:00 +000069 keyring := agent.NewKeyring()
70 err = keyring.Add(agent.AddedKey{
71 PrivateKey: privKey,
72 })
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010073 if err != nil {
74 t.Fatal(err)
75 }
76
Jan Schärec61a472025-03-24 18:54:00 +000077 // Create the socket directory. We keep it in /tmp because of socket path limits.
78 socketDir, err := os.MkdirTemp("/tmp", "test-sockets-*")
79 if err != nil {
80 t.Fatalf("Failed to create socket directory: %v", err)
81 }
82 defer os.RemoveAll(socketDir)
83
84 // Start ssh agent server.
85 sshAuthSock := socketDir + "/ssh-auth"
86 agentListener, err := net.ListenUnix("unix", &net.UnixAddr{Name: sshAuthSock, Net: "unix"})
87 if err != nil {
88 t.Fatal(err)
89 }
90 go func() {
91 for {
92 conn, err := agentListener.AcceptUnix()
93 if err != nil {
94 return
95 }
96 err = agent.ServeAgent(keyring, conn)
97 if err != nil && !errors.Is(err, io.EOF) {
98 t.Logf("ServeAgent error: %v", err)
99 }
100 conn.Close()
101 }
102 }()
103
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100104 // CloudConfig doesn't really have a rigid spec, so just put things into it
105 cloudConfig := make(map[string]any)
106 cloudConfig["ssh_authorized_keys"] = []string{
107 strings.TrimSuffix(string(xssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
108 }
Jan Schärec61a472025-03-24 18:54:00 +0000109 cloudConfig["disable_root"] = false
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100110
111 userData, err := json.Marshal(cloudConfig)
112 if err != nil {
113 t.Fatal(err)
114 }
115
Jan Schärc1b6df42025-03-20 08:52:18 +0000116 root := structfs.Tree{
117 structfs.File("user-data", structfs.Bytes("#cloud-config\n"+string(userData))),
118 structfs.File("meta-data", structfs.Bytes("")),
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100119 }
120 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
121 if err != nil {
122 t.Fatal(err)
123 }
124 defer os.Remove(cloudInitDataFile.Name())
Jan Schärc1b6df42025-03-20 08:52:18 +0000125 if err := fat32.WriteFS(cloudInitDataFile, root, fat32.Options{Label: "cidata"}); err != nil {
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100126 t.Fatal(err)
127 }
128
129 rootDisk, err := os.CreateTemp("", "rootdisk")
130 if err != nil {
131 t.Fatal(err)
132 }
133 // Create a 10GiB sparse root disk
134 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
135 t.Fatalf("ftruncate failed: %v", err)
136 }
137
138 defer os.Remove(rootDisk.Name())
139
140 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
141 if err != nil {
142 t.Fatal(err)
143 }
144
145 qemuArgs := []string{
146 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
147 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
148 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
149 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
150 "-drive", "if=none,format=raw,cache=unsafe,id=root,file=" + rootDisk.Name(),
151 "-drive", "if=none,format=qcow2,snapshot=on,id=cloud,cache=unsafe,file=" + xCloudImagePath,
152 "-device", "virtio-blk-pci,drive=root,bootindex=1",
153 "-device", "virtio-blk-pci,drive=cloud,bootindex=2",
154 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
155 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
156 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
157 "-device", "virtio-rng-pci",
158 "-serial", "stdio",
159 }
160 qemuCmd := exec.Command("qemu-system-x86_64", qemuArgs...)
161 stdoutPipe, err := qemuCmd.StdoutPipe()
162 if err != nil {
163 t.Fatal(err)
164 }
Jan Schärec61a472025-03-24 18:54:00 +0000165 sshdStarted := make(chan struct{})
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100166 installSucceed := make(chan struct{})
167 go func() {
168 s := bufio.NewScanner(stdoutPipe)
169 for s.Scan() {
Jan Schärec61a472025-03-24 18:54:00 +0000170 t.Logf("VM: %q", s.Text())
171 if strings.Contains(s.Text(), "Started") &&
172 strings.Contains(s.Text(), "Secure Shell server") {
173 sshdStarted <- struct{}{}
174 break
175 }
176 }
177 for s.Scan() {
178 t.Logf("VM: %q", s.Text())
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100179 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
180 installSucceed <- struct{}{}
181 break
182 }
183 }
184 qemuCmd.Wait()
185 }()
186 qemuCmd.Stderr = os.Stderr
187 sshPortCloser.Close()
188 if err := qemuCmd.Start(); err != nil {
189 t.Fatal(err)
190 }
191 defer qemuCmd.Process.Kill()
192
Jan Schärec61a472025-03-24 18:54:00 +0000193 select {
194 case <-sshdStarted:
195 case <-time.After(30 * time.Second):
196 t.Fatal("Waiting for sshd start timed out")
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100197 }
198
Jan Schärec61a472025-03-24 18:54:00 +0000199 installArgs := []string{
200 "install", "ssh",
201 fmt.Sprintf("root@localhost:%d", sshPort),
202 "--disk", "vda",
203 "--bootstrap",
204 "--cluster", "cluster.internal",
205 "--takeover", xTakeoverPath,
206 "--bundle", xBundleFilePath,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100207 }
Jan Schärec61a472025-03-24 18:54:00 +0000208 installCmd := exec.Command(xMetroctlPath, installArgs...)
209 installCmd.Env = append(installCmd.Environ(), fmt.Sprintf("SSH_AUTH_SOCK=%s", sshAuthSock))
210 installCmd.Stdout = os.Stdout
211 installCmd.Stderr = os.Stderr
212 t.Logf("Running %s", installCmd.String())
213 if err := installCmd.Run(); err != nil {
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100214 t.Fatal(err)
215 }
216
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100217 select {
218 case <-installSucceed:
219 // Done, test passed
220 case <-time.After(30 * time.Second):
221 t.Fatal("Waiting for installation timed out")
222 }
223}