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