blob: 1ba5354d4392710f7a4f13d173382befa6da620a [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"
28)
29
30var (
31 // These are filled by bazel at linking time with the canonical path of
32 // their corresponding file. Inside the init function we resolve it
33 // with the rules_go runfiles package to the real path.
34 xBundleFilePath string
35 xOvmfVarsPath string
36 xOvmfCodePath string
37 xCloudImagePath string
38 xTakeoverPath string
Jan Schärec61a472025-03-24 18:54:00 +000039 xMetroctlPath string
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010040)
41
42func init() {
43 var err error
44 for _, path := range []*string{
45 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
Jan Schärec61a472025-03-24 18:54:00 +000046 &xTakeoverPath, &xBundleFilePath, &xMetroctlPath,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010047 } {
48 *path, err = runfiles.Rlocation(*path)
49 if err != nil {
50 panic(err)
51 }
52 }
53}
54
55const GiB = 1024 * 1024 * 1024
56
57func TestE2E(t *testing.T) {
58 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
59 if err != nil {
60 t.Fatal(err)
61 }
62
63 sshPubKey, err := xssh.NewPublicKey(pubKey)
64 if err != nil {
65 t.Fatal(err)
66 }
67
Jan Schärec61a472025-03-24 18:54:00 +000068 keyring := agent.NewKeyring()
69 err = keyring.Add(agent.AddedKey{
70 PrivateKey: privKey,
71 })
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +010072 if err != nil {
73 t.Fatal(err)
74 }
75
Jan Schärec61a472025-03-24 18:54:00 +000076 // Create the socket directory. We keep it in /tmp because of socket path limits.
77 socketDir, err := os.MkdirTemp("/tmp", "test-sockets-*")
78 if err != nil {
79 t.Fatalf("Failed to create socket directory: %v", err)
80 }
81 defer os.RemoveAll(socketDir)
82
83 // Start ssh agent server.
84 sshAuthSock := socketDir + "/ssh-auth"
85 agentListener, err := net.ListenUnix("unix", &net.UnixAddr{Name: sshAuthSock, Net: "unix"})
86 if err != nil {
87 t.Fatal(err)
88 }
89 go func() {
90 for {
91 conn, err := agentListener.AcceptUnix()
92 if err != nil {
93 return
94 }
95 err = agent.ServeAgent(keyring, conn)
96 if err != nil && !errors.Is(err, io.EOF) {
97 t.Logf("ServeAgent error: %v", err)
98 }
99 conn.Close()
100 }
101 }()
102
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100103 // CloudConfig doesn't really have a rigid spec, so just put things into it
104 cloudConfig := make(map[string]any)
105 cloudConfig["ssh_authorized_keys"] = []string{
106 strings.TrimSuffix(string(xssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
107 }
Jan Schärec61a472025-03-24 18:54:00 +0000108 cloudConfig["disable_root"] = false
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100109
110 userData, err := json.Marshal(cloudConfig)
111 if err != nil {
112 t.Fatal(err)
113 }
114
115 rootInode := fat32.Inode{
116 Attrs: fat32.AttrDirectory,
117 Children: []*fat32.Inode{
118 {
119 Name: "user-data",
120 Content: strings.NewReader("#cloud-config\n" + string(userData)),
121 },
122 {
123 Name: "meta-data",
124 Content: strings.NewReader(""),
125 },
126 },
127 }
128 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
129 if err != nil {
130 t.Fatal(err)
131 }
132 defer os.Remove(cloudInitDataFile.Name())
133 if err := fat32.WriteFS(cloudInitDataFile, rootInode, fat32.Options{Label: "cidata"}); err != nil {
134 t.Fatal(err)
135 }
136
137 rootDisk, err := os.CreateTemp("", "rootdisk")
138 if err != nil {
139 t.Fatal(err)
140 }
141 // Create a 10GiB sparse root disk
142 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
143 t.Fatalf("ftruncate failed: %v", err)
144 }
145
146 defer os.Remove(rootDisk.Name())
147
148 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
149 if err != nil {
150 t.Fatal(err)
151 }
152
153 qemuArgs := []string{
154 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
155 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
156 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
157 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
158 "-drive", "if=none,format=raw,cache=unsafe,id=root,file=" + rootDisk.Name(),
159 "-drive", "if=none,format=qcow2,snapshot=on,id=cloud,cache=unsafe,file=" + xCloudImagePath,
160 "-device", "virtio-blk-pci,drive=root,bootindex=1",
161 "-device", "virtio-blk-pci,drive=cloud,bootindex=2",
162 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
163 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
164 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
165 "-device", "virtio-rng-pci",
166 "-serial", "stdio",
167 }
168 qemuCmd := exec.Command("qemu-system-x86_64", qemuArgs...)
169 stdoutPipe, err := qemuCmd.StdoutPipe()
170 if err != nil {
171 t.Fatal(err)
172 }
Jan Schärec61a472025-03-24 18:54:00 +0000173 sshdStarted := make(chan struct{})
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100174 installSucceed := make(chan struct{})
175 go func() {
176 s := bufio.NewScanner(stdoutPipe)
177 for s.Scan() {
Jan Schärec61a472025-03-24 18:54:00 +0000178 t.Logf("VM: %q", s.Text())
179 if strings.Contains(s.Text(), "Started") &&
180 strings.Contains(s.Text(), "Secure Shell server") {
181 sshdStarted <- struct{}{}
182 break
183 }
184 }
185 for s.Scan() {
186 t.Logf("VM: %q", s.Text())
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100187 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
188 installSucceed <- struct{}{}
189 break
190 }
191 }
192 qemuCmd.Wait()
193 }()
194 qemuCmd.Stderr = os.Stderr
195 sshPortCloser.Close()
196 if err := qemuCmd.Start(); err != nil {
197 t.Fatal(err)
198 }
199 defer qemuCmd.Process.Kill()
200
Jan Schärec61a472025-03-24 18:54:00 +0000201 select {
202 case <-sshdStarted:
203 case <-time.After(30 * time.Second):
204 t.Fatal("Waiting for sshd start timed out")
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100205 }
206
Jan Schärec61a472025-03-24 18:54:00 +0000207 installArgs := []string{
208 "install", "ssh",
209 fmt.Sprintf("root@localhost:%d", sshPort),
210 "--disk", "vda",
211 "--bootstrap",
212 "--cluster", "cluster.internal",
213 "--takeover", xTakeoverPath,
214 "--bundle", xBundleFilePath,
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100215 }
Jan Schärec61a472025-03-24 18:54:00 +0000216 installCmd := exec.Command(xMetroctlPath, installArgs...)
217 installCmd.Env = append(installCmd.Environ(), fmt.Sprintf("SSH_AUTH_SOCK=%s", sshAuthSock))
218 installCmd.Stdout = os.Stdout
219 installCmd.Stderr = os.Stderr
220 t.Logf("Running %s", installCmd.String())
221 if err := installCmd.Run(); err != nil {
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100222 t.Fatal(err)
223 }
224
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +0100225 select {
226 case <-installSucceed:
227 // Done, test passed
228 case <-time.After(30 * time.Second):
229 t.Fatal("Waiting for installation timed out")
230 }
231}