blob: c86a48d54633efa3d637fc3285c718bb7e4adf25 [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 Windelschmidt4bd25e82025-07-11 19:36:28 +020017 "path/filepath"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010018 "strings"
19 "testing"
20 "time"
21
22 "github.com/bazelbuild/rules_go/go/runfiles"
23 xssh "golang.org/x/crypto/ssh"
Jan Schärec61a472025-03-24 18:54:00 +000024 "golang.org/x/crypto/ssh/agent"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010025 "golang.org/x/sys/unix"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010026
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010027 "source.monogon.dev/osbase/fat32"
Jan Schärc1b6df42025-03-20 08:52:18 +000028 "source.monogon.dev/osbase/structfs"
Jan Schär341cd422025-09-04 10:33:21 +020029 "source.monogon.dev/osbase/test/freeport"
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010030)
31
32var (
33 // These are filled by bazel at linking time with the canonical path of
34 // their corresponding file. Inside the init function we resolve it
35 // with the rules_go runfiles package to the real path.
Jan Schär5fdca562025-04-14 11:33:29 +000036 xImagePath string
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010037 xOvmfVarsPath string
38 xOvmfCodePath string
39 xCloudImagePath string
40 xTakeoverPath string
Jan Schärec61a472025-03-24 18:54:00 +000041 xMetroctlPath string
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020042 xQEMUPath string
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010043)
44
45func init() {
46 var err error
47 for _, path := range []*string{
48 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
Jan Schär5fdca562025-04-14 11:33:29 +000049 &xTakeoverPath, &xImagePath, &xMetroctlPath,
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020050 &xQEMUPath,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010051 } {
52 *path, err = runfiles.Rlocation(*path)
53 if err != nil {
54 panic(err)
55 }
56 }
Tim Windelschmidt4bd25e82025-07-11 19:36:28 +020057 // When running QEMU with snapshot=on set, QEMU creates a copy of the
58 // provided file in $TMPDIR. If $TMPDIR is set to /tmp, it will always
59 // be overridden to /var/tmp. This creates an issue for us, as the
60 // bazel tests only wire up /tmp, with /var/tmp being unaccessible
61 // because of permissions. Bazel provides $TEST_TMPDIR for this
62 // usecase, which we resolve to an absolute path and then override
63 // $TMPDIR.
64 tmpDir, err := filepath.Abs(os.Getenv("TEST_TMPDIR"))
65 if err != nil {
66 panic(err)
67 }
68 os.Setenv("TMPDIR", tmpDir)
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010069}
70
71const GiB = 1024 * 1024 * 1024
72
73func TestE2E(t *testing.T) {
74 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
75 if err != nil {
76 t.Fatal(err)
77 }
78
79 sshPubKey, err := xssh.NewPublicKey(pubKey)
80 if err != nil {
81 t.Fatal(err)
82 }
83
Jan Schärec61a472025-03-24 18:54:00 +000084 keyring := agent.NewKeyring()
85 err = keyring.Add(agent.AddedKey{
86 PrivateKey: privKey,
87 })
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010088 if err != nil {
89 t.Fatal(err)
90 }
91
Jan Schärec61a472025-03-24 18:54:00 +000092 // Create the socket directory. We keep it in /tmp because of socket path limits.
93 socketDir, err := os.MkdirTemp("/tmp", "test-sockets-*")
94 if err != nil {
95 t.Fatalf("Failed to create socket directory: %v", err)
96 }
97 defer os.RemoveAll(socketDir)
98
99 // Start ssh agent server.
100 sshAuthSock := socketDir + "/ssh-auth"
101 agentListener, err := net.ListenUnix("unix", &net.UnixAddr{Name: sshAuthSock, Net: "unix"})
102 if err != nil {
103 t.Fatal(err)
104 }
105 go func() {
106 for {
107 conn, err := agentListener.AcceptUnix()
108 if err != nil {
109 return
110 }
111 err = agent.ServeAgent(keyring, conn)
112 if err != nil && !errors.Is(err, io.EOF) {
113 t.Logf("ServeAgent error: %v", err)
114 }
115 conn.Close()
116 }
117 }()
118
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100119 // CloudConfig doesn't really have a rigid spec, so just put things into it
120 cloudConfig := make(map[string]any)
121 cloudConfig["ssh_authorized_keys"] = []string{
122 strings.TrimSuffix(string(xssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
123 }
Jan Schärec61a472025-03-24 18:54:00 +0000124 cloudConfig["disable_root"] = false
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100125
126 userData, err := json.Marshal(cloudConfig)
127 if err != nil {
128 t.Fatal(err)
129 }
130
Jan Schärc1b6df42025-03-20 08:52:18 +0000131 root := structfs.Tree{
132 structfs.File("user-data", structfs.Bytes("#cloud-config\n"+string(userData))),
133 structfs.File("meta-data", structfs.Bytes("")),
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100134 }
135 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
136 if err != nil {
137 t.Fatal(err)
138 }
139 defer os.Remove(cloudInitDataFile.Name())
Jan Schärc1b6df42025-03-20 08:52:18 +0000140 if err := fat32.WriteFS(cloudInitDataFile, root, fat32.Options{Label: "cidata"}); err != nil {
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100141 t.Fatal(err)
142 }
143
144 rootDisk, err := os.CreateTemp("", "rootdisk")
145 if err != nil {
146 t.Fatal(err)
147 }
148 // Create a 10GiB sparse root disk
149 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
150 t.Fatalf("ftruncate failed: %v", err)
151 }
152
153 defer os.Remove(rootDisk.Name())
154
155 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
156 if err != nil {
157 t.Fatal(err)
158 }
159
160 qemuArgs := []string{
161 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
162 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
163 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
164 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
165 "-drive", "if=none,format=raw,cache=unsafe,id=root,file=" + rootDisk.Name(),
166 "-drive", "if=none,format=qcow2,snapshot=on,id=cloud,cache=unsafe,file=" + xCloudImagePath,
167 "-device", "virtio-blk-pci,drive=root,bootindex=1",
168 "-device", "virtio-blk-pci,drive=cloud,bootindex=2",
169 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
170 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
171 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
172 "-device", "virtio-rng-pci",
173 "-serial", "stdio",
174 }
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +0200175 qemuCmd := exec.Command(xQEMUPath, qemuArgs...)
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100176 stdoutPipe, err := qemuCmd.StdoutPipe()
177 if err != nil {
178 t.Fatal(err)
179 }
Jan Schärec61a472025-03-24 18:54:00 +0000180 sshdStarted := make(chan struct{})
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100181 installSucceed := make(chan struct{})
182 go func() {
183 s := bufio.NewScanner(stdoutPipe)
184 for s.Scan() {
Jan Schärec61a472025-03-24 18:54:00 +0000185 t.Logf("VM: %q", s.Text())
186 if strings.Contains(s.Text(), "Started") &&
187 strings.Contains(s.Text(), "Secure Shell server") {
188 sshdStarted <- struct{}{}
189 break
190 }
191 }
192 for s.Scan() {
193 t.Logf("VM: %q", s.Text())
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100194 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
195 installSucceed <- struct{}{}
196 break
197 }
198 }
199 qemuCmd.Wait()
200 }()
201 qemuCmd.Stderr = os.Stderr
202 sshPortCloser.Close()
203 if err := qemuCmd.Start(); err != nil {
204 t.Fatal(err)
205 }
206 defer qemuCmd.Process.Kill()
207
Jan Schärec61a472025-03-24 18:54:00 +0000208 select {
209 case <-sshdStarted:
210 case <-time.After(30 * time.Second):
211 t.Fatal("Waiting for sshd start timed out")
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100212 }
213
Tim Windelschmidt0a2a9402025-07-08 01:55:59 +0200214 // Create the config directory. We keep it in /tmp because of sandbox limitations.
215 configDir, err := os.MkdirTemp("/tmp", "test-configs-*")
216 if err != nil {
217 t.Fatalf("Failed to create config directory: %v", err)
218 }
219 defer os.RemoveAll(configDir)
220
Jan Schärec61a472025-03-24 18:54:00 +0000221 installArgs := []string{
222 "install", "ssh",
223 fmt.Sprintf("root@localhost:%d", sshPort),
224 "--disk", "vda",
225 "--bootstrap",
226 "--cluster", "cluster.internal",
227 "--takeover", xTakeoverPath,
Jan Schär5fdca562025-04-14 11:33:29 +0000228 "--image", xImagePath,
Tim Windelschmidt0a2a9402025-07-08 01:55:59 +0200229 "--config", configDir,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100230 }
Jan Schärec61a472025-03-24 18:54:00 +0000231 installCmd := exec.Command(xMetroctlPath, installArgs...)
232 installCmd.Env = append(installCmd.Environ(), fmt.Sprintf("SSH_AUTH_SOCK=%s", sshAuthSock))
233 installCmd.Stdout = os.Stdout
234 installCmd.Stderr = os.Stderr
235 t.Logf("Running %s", installCmd.String())
236 if err := installCmd.Run(); err != nil {
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100237 t.Fatal(err)
238 }
239
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100240 select {
241 case <-installSucceed:
242 // Done, test passed
243 case <-time.After(30 * time.Second):
244 t.Fatal("Waiting for installation timed out")
245 }
246}