blob: 482752abef42459b1b9b416469860ffd7444371b [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.
Jan Schär5fdca562025-04-14 11:33:29 +000035 xImagePath string
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010036 xOvmfVarsPath string
37 xOvmfCodePath string
38 xCloudImagePath string
39 xTakeoverPath string
Jan Schärec61a472025-03-24 18:54:00 +000040 xMetroctlPath string
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020041 xQEMUPath string
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010042)
43
44func init() {
45 var err error
46 for _, path := range []*string{
47 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
Jan Schär5fdca562025-04-14 11:33:29 +000048 &xTakeoverPath, &xImagePath, &xMetroctlPath,
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020049 &xQEMUPath,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010050 } {
51 *path, err = runfiles.Rlocation(*path)
52 if err != nil {
53 panic(err)
54 }
55 }
56}
57
58const GiB = 1024 * 1024 * 1024
59
60func TestE2E(t *testing.T) {
61 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
62 if err != nil {
63 t.Fatal(err)
64 }
65
66 sshPubKey, err := xssh.NewPublicKey(pubKey)
67 if err != nil {
68 t.Fatal(err)
69 }
70
Jan Schärec61a472025-03-24 18:54:00 +000071 keyring := agent.NewKeyring()
72 err = keyring.Add(agent.AddedKey{
73 PrivateKey: privKey,
74 })
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010075 if err != nil {
76 t.Fatal(err)
77 }
78
Jan Schärec61a472025-03-24 18:54:00 +000079 // Create the socket directory. We keep it in /tmp because of socket path limits.
80 socketDir, err := os.MkdirTemp("/tmp", "test-sockets-*")
81 if err != nil {
82 t.Fatalf("Failed to create socket directory: %v", err)
83 }
84 defer os.RemoveAll(socketDir)
85
86 // Start ssh agent server.
87 sshAuthSock := socketDir + "/ssh-auth"
88 agentListener, err := net.ListenUnix("unix", &net.UnixAddr{Name: sshAuthSock, Net: "unix"})
89 if err != nil {
90 t.Fatal(err)
91 }
92 go func() {
93 for {
94 conn, err := agentListener.AcceptUnix()
95 if err != nil {
96 return
97 }
98 err = agent.ServeAgent(keyring, conn)
99 if err != nil && !errors.Is(err, io.EOF) {
100 t.Logf("ServeAgent error: %v", err)
101 }
102 conn.Close()
103 }
104 }()
105
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100106 // CloudConfig doesn't really have a rigid spec, so just put things into it
107 cloudConfig := make(map[string]any)
108 cloudConfig["ssh_authorized_keys"] = []string{
109 strings.TrimSuffix(string(xssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
110 }
Jan Schärec61a472025-03-24 18:54:00 +0000111 cloudConfig["disable_root"] = false
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100112
113 userData, err := json.Marshal(cloudConfig)
114 if err != nil {
115 t.Fatal(err)
116 }
117
Jan Schärc1b6df42025-03-20 08:52:18 +0000118 root := structfs.Tree{
119 structfs.File("user-data", structfs.Bytes("#cloud-config\n"+string(userData))),
120 structfs.File("meta-data", structfs.Bytes("")),
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100121 }
122 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
123 if err != nil {
124 t.Fatal(err)
125 }
126 defer os.Remove(cloudInitDataFile.Name())
Jan Schärc1b6df42025-03-20 08:52:18 +0000127 if err := fat32.WriteFS(cloudInitDataFile, root, fat32.Options{Label: "cidata"}); err != nil {
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100128 t.Fatal(err)
129 }
130
131 rootDisk, err := os.CreateTemp("", "rootdisk")
132 if err != nil {
133 t.Fatal(err)
134 }
135 // Create a 10GiB sparse root disk
136 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
137 t.Fatalf("ftruncate failed: %v", err)
138 }
139
140 defer os.Remove(rootDisk.Name())
141
142 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
143 if err != nil {
144 t.Fatal(err)
145 }
146
147 qemuArgs := []string{
148 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
149 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
150 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
151 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
152 "-drive", "if=none,format=raw,cache=unsafe,id=root,file=" + rootDisk.Name(),
153 "-drive", "if=none,format=qcow2,snapshot=on,id=cloud,cache=unsafe,file=" + xCloudImagePath,
154 "-device", "virtio-blk-pci,drive=root,bootindex=1",
155 "-device", "virtio-blk-pci,drive=cloud,bootindex=2",
156 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
157 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
158 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
159 "-device", "virtio-rng-pci",
160 "-serial", "stdio",
161 }
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +0200162 qemuCmd := exec.Command(xQEMUPath, qemuArgs...)
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100163 stdoutPipe, err := qemuCmd.StdoutPipe()
164 if err != nil {
165 t.Fatal(err)
166 }
Jan Schärec61a472025-03-24 18:54:00 +0000167 sshdStarted := make(chan struct{})
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100168 installSucceed := make(chan struct{})
169 go func() {
170 s := bufio.NewScanner(stdoutPipe)
171 for s.Scan() {
Jan Schärec61a472025-03-24 18:54:00 +0000172 t.Logf("VM: %q", s.Text())
173 if strings.Contains(s.Text(), "Started") &&
174 strings.Contains(s.Text(), "Secure Shell server") {
175 sshdStarted <- struct{}{}
176 break
177 }
178 }
179 for s.Scan() {
180 t.Logf("VM: %q", s.Text())
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100181 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
182 installSucceed <- struct{}{}
183 break
184 }
185 }
186 qemuCmd.Wait()
187 }()
188 qemuCmd.Stderr = os.Stderr
189 sshPortCloser.Close()
190 if err := qemuCmd.Start(); err != nil {
191 t.Fatal(err)
192 }
193 defer qemuCmd.Process.Kill()
194
Jan Schärec61a472025-03-24 18:54:00 +0000195 select {
196 case <-sshdStarted:
197 case <-time.After(30 * time.Second):
198 t.Fatal("Waiting for sshd start timed out")
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100199 }
200
Tim Windelschmidt0a2a9402025-07-08 01:55:59 +0200201 // Create the config directory. We keep it in /tmp because of sandbox limitations.
202 configDir, err := os.MkdirTemp("/tmp", "test-configs-*")
203 if err != nil {
204 t.Fatalf("Failed to create config directory: %v", err)
205 }
206 defer os.RemoveAll(configDir)
207
Jan Schärec61a472025-03-24 18:54:00 +0000208 installArgs := []string{
209 "install", "ssh",
210 fmt.Sprintf("root@localhost:%d", sshPort),
211 "--disk", "vda",
212 "--bootstrap",
213 "--cluster", "cluster.internal",
214 "--takeover", xTakeoverPath,
Jan Schär5fdca562025-04-14 11:33:29 +0000215 "--image", xImagePath,
Tim Windelschmidt0a2a9402025-07-08 01:55:59 +0200216 "--config", configDir,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100217 }
Jan Schärec61a472025-03-24 18:54:00 +0000218 installCmd := exec.Command(xMetroctlPath, installArgs...)
219 installCmd.Env = append(installCmd.Environ(), fmt.Sprintf("SSH_AUTH_SOCK=%s", sshAuthSock))
220 installCmd.Stdout = os.Stdout
221 installCmd.Stderr = os.Stderr
222 t.Logf("Running %s", installCmd.String())
223 if err := installCmd.Run(); err != nil {
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100224 t.Fatal(err)
225 }
226
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100227 select {
228 case <-installSucceed:
229 // Done, test passed
230 case <-time.After(30 * time.Second):
231 t.Fatal("Waiting for installation timed out")
232 }
233}