blob: c9108cd7a982ac58b126be99fcf42e35c61d6dd0 [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"
8 "context"
9 "crypto/ed25519"
10 "crypto/rand"
11 "encoding/json"
12 "fmt"
13 "net"
14 "os"
15 "os/exec"
16 "os/signal"
17 "strings"
18 "testing"
19 "time"
20
21 "github.com/bazelbuild/rules_go/go/runfiles"
22 xssh "golang.org/x/crypto/ssh"
23 "golang.org/x/sys/unix"
24 "google.golang.org/protobuf/proto"
25
26 "source.monogon.dev/metropolis/proto/api"
27
28 "source.monogon.dev/go/net/ssh"
29 "source.monogon.dev/metropolis/test/launch"
30 "source.monogon.dev/osbase/fat32"
31 "source.monogon.dev/osbase/freeport"
32)
33
34var (
35 // These are filled by bazel at linking time with the canonical path of
36 // their corresponding file. Inside the init function we resolve it
37 // with the rules_go runfiles package to the real path.
38 xBundleFilePath string
39 xOvmfVarsPath string
40 xOvmfCodePath string
41 xCloudImagePath string
42 xTakeoverPath string
43)
44
45func init() {
46 var err error
47 for _, path := range []*string{
48 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
49 &xTakeoverPath, &xBundleFilePath,
50 } {
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
71 sshPrivkey, err := xssh.NewSignerFromKey(privKey)
72 if err != nil {
73 t.Fatal(err)
74 }
75
76 // CloudConfig doesn't really have a rigid spec, so just put things into it
77 cloudConfig := make(map[string]any)
78 cloudConfig["ssh_authorized_keys"] = []string{
79 strings.TrimSuffix(string(xssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
80 }
81
82 userData, err := json.Marshal(cloudConfig)
83 if err != nil {
84 t.Fatal(err)
85 }
86
87 rootInode := fat32.Inode{
88 Attrs: fat32.AttrDirectory,
89 Children: []*fat32.Inode{
90 {
91 Name: "user-data",
92 Content: strings.NewReader("#cloud-config\n" + string(userData)),
93 },
94 {
95 Name: "meta-data",
96 Content: strings.NewReader(""),
97 },
98 },
99 }
100 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
101 if err != nil {
102 t.Fatal(err)
103 }
104 defer os.Remove(cloudInitDataFile.Name())
105 if err := fat32.WriteFS(cloudInitDataFile, rootInode, fat32.Options{Label: "cidata"}); err != nil {
106 t.Fatal(err)
107 }
108
109 rootDisk, err := os.CreateTemp("", "rootdisk")
110 if err != nil {
111 t.Fatal(err)
112 }
113 // Create a 10GiB sparse root disk
114 if err := unix.Ftruncate(int(rootDisk.Fd()), 10*GiB); err != nil {
115 t.Fatalf("ftruncate failed: %v", err)
116 }
117
118 defer os.Remove(rootDisk.Name())
119
120 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
121 if err != nil {
122 t.Fatal(err)
123 }
124
125 qemuArgs := []string{
126 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
127 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
128 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
129 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
130 "-drive", "if=none,format=raw,cache=unsafe,id=root,file=" + rootDisk.Name(),
131 "-drive", "if=none,format=qcow2,snapshot=on,id=cloud,cache=unsafe,file=" + xCloudImagePath,
132 "-device", "virtio-blk-pci,drive=root,bootindex=1",
133 "-device", "virtio-blk-pci,drive=cloud,bootindex=2",
134 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
135 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
136 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
137 "-device", "virtio-rng-pci",
138 "-serial", "stdio",
139 }
140 qemuCmd := exec.Command("qemu-system-x86_64", qemuArgs...)
141 stdoutPipe, err := qemuCmd.StdoutPipe()
142 if err != nil {
143 t.Fatal(err)
144 }
145 installSucceed := make(chan struct{})
146 go func() {
147 s := bufio.NewScanner(stdoutPipe)
148 for s.Scan() {
149 t.Log("kernel: " + s.Text())
150 if strings.Contains(s.Text(), "_TESTOS_LAUNCH_SUCCESS_") {
151 installSucceed <- struct{}{}
152 break
153 }
154 }
155 qemuCmd.Wait()
156 }()
157 qemuCmd.Stderr = os.Stderr
158 sshPortCloser.Close()
159 if err := qemuCmd.Start(); err != nil {
160 t.Fatal(err)
161 }
162 defer qemuCmd.Process.Kill()
163
164 cl := ssh.DirectClient{
165 Username: "debian",
166 AuthMethods: []xssh.AuthMethod{xssh.PublicKeys(sshPrivkey)},
167 }
168
169 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
170
171 var conn ssh.Connection
172 for {
173 conn, err = cl.Dial(ctx, net.JoinHostPort("localhost", fmt.Sprintf("%d", sshPort)), 5*time.Second)
174 if err != nil {
175 t.Logf("error connecting via SSH, retrying: %v", err)
176 time.Sleep(1 * time.Second)
177 continue
178 }
179 break
180 }
181
182 takeover, err := os.Open(xTakeoverPath)
183 if err != nil {
184 t.Fatal(err)
185 }
186
187 const takeoverTargetPath = "/tmp/takeover"
188 if err := conn.Upload(ctx, takeoverTargetPath, takeover); err != nil {
189 t.Fatalf("error while uploading takeover: %v", err)
190 }
191
192 bundleFile, err := os.Open(xBundleFilePath)
193 if err != nil {
194 t.Fatal(err)
195 }
196
197 const bundleTargetPath = "/tmp/bundle.zip"
198 if err := conn.Upload(ctx, bundleTargetPath, bundleFile); err != nil {
199 t.Fatalf("error while uploading bundle: %v", err)
200 }
201
202 params := &api.NodeParameters{
203 Cluster: &api.NodeParameters_ClusterBootstrap_{
204 ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
205 OwnerPublicKey: launch.InsecurePublicKey,
206 },
207 },
208 NetworkConfig: nil,
209 }
210 rawParams, err := proto.Marshal(params)
211 if err != nil {
212 t.Fatalf("error while marshaling node params: %v", err)
213 }
214
215 // Start the agent and wait for the agent's output to arrive.
216 t.Logf("Starting the takeover executable at path %q.", takeoverTargetPath)
217 _, stderr, err := conn.Execute(ctx, fmt.Sprintf("sudo %s -disk %s", takeoverTargetPath, "vda"), rawParams)
218 stderrStr := strings.TrimSpace(string(stderr))
219 if stderrStr != "" {
220 t.Logf("Agent stderr: %q", stderrStr)
221 }
222 if err != nil {
223 t.Fatalf("while starting the takeover executable: %v", err)
224 }
225
226 select {
227 case <-installSucceed:
228 // Done, test passed
229 case <-time.After(30 * time.Second):
230 t.Fatal("Waiting for installation timed out")
231 }
232}