blob: 5387f9287d3dd1d450505de88346d360552efd33 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun2d284b52023-03-08 17:05:12 +01004package e2e
5
6import (
7 "bufio"
8 "bytes"
9 "crypto/ed25519"
10 "crypto/rand"
11 "encoding/json"
12 "fmt"
13 "io"
14 "net"
15 "os"
16 "os/exec"
17 "strings"
18 "testing"
19 "time"
20
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010021 "github.com/bazelbuild/rules_go/go/runfiles"
Lorenz Brun2d284b52023-03-08 17:05:12 +010022 "github.com/pkg/sftp"
23 "golang.org/x/crypto/ssh"
24 "google.golang.org/protobuf/proto"
25
26 "source.monogon.dev/cloud/agent/api"
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010027
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020028 "source.monogon.dev/osbase/fat32"
29 "source.monogon.dev/osbase/freeport"
Jan Schärc1b6df42025-03-20 08:52:18 +000030 "source.monogon.dev/osbase/structfs"
Lorenz Brun2d284b52023-03-08 17:05:12 +010031)
32
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000033var (
34 // These are filled by bazel at linking time with the canonical path of
35 // their corresponding file. Inside the init function we resolve it
36 // with the rules_go runfiles package to the real path.
37 xCloudImagePath string
38 xOvmfVarsPath string
39 xOvmfCodePath string
40 xTakeoverPath string
41)
42
43func init() {
44 var err error
45 for _, path := range []*string{
46 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
47 &xTakeoverPath,
48 } {
49 *path, err = runfiles.Rlocation(*path)
50 if err != nil {
51 panic(err)
52 }
53 }
54}
55
Lorenz Brun2d284b52023-03-08 17:05:12 +010056func TestE2E(t *testing.T) {
57 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
58 if err != nil {
59 t.Fatal(err)
60 }
61
62 sshPubKey, err := ssh.NewPublicKey(pubKey)
63 if err != nil {
64 t.Fatal(err)
65 }
66
67 sshPrivkey, err := ssh.NewSignerFromKey(privKey)
68 if err != nil {
69 t.Fatal(err)
70 }
71
72 // CloudConfig doesn't really have a rigid spec, so just put things into it
73 cloudConfig := make(map[string]any)
74 cloudConfig["ssh_authorized_keys"] = []string{
75 strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
76 }
77
78 userData, err := json.Marshal(cloudConfig)
79 if err != nil {
80 t.Fatal(err)
81 }
82
Jan Schärc1b6df42025-03-20 08:52:18 +000083 root := structfs.Tree{
84 structfs.File("user-data", structfs.Bytes("#cloud-config\n"+string(userData))),
85 structfs.File("meta-data", structfs.Bytes("")),
Lorenz Brun2d284b52023-03-08 17:05:12 +010086 }
87 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
88 if err != nil {
89 t.Fatal(err)
90 }
91 defer os.Remove(cloudInitDataFile.Name())
Jan Schärc1b6df42025-03-20 08:52:18 +000092 if err := fat32.WriteFS(cloudInitDataFile, root, fat32.Options{Label: "cidata"}); err != nil {
Lorenz Brun2d284b52023-03-08 17:05:12 +010093 t.Fatal(err)
94 }
Lorenz Brun2d284b52023-03-08 17:05:12 +010095
96 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
97 if err != nil {
98 t.Fatal(err)
99 }
100
101 qemuArgs := []string{
102 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
103 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000104 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
105 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
106 "-drive", "if=virtio,format=qcow2,snapshot=on,cache=unsafe,file=" + xCloudImagePath,
Lorenz Brun2d284b52023-03-08 17:05:12 +0100107 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
108 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
109 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
110 "-device", "virtio-rng-pci",
111 "-serial", "stdio",
112 "-no-reboot",
113 }
114 qemuCmd := exec.Command("qemu-system-x86_64", qemuArgs...)
115 stdoutPipe, err := qemuCmd.StdoutPipe()
116 if err != nil {
117 t.Fatal(err)
118 }
119 agentStarted := make(chan struct{})
120 go func() {
121 s := bufio.NewScanner(stdoutPipe)
122 for s.Scan() {
123 t.Log("kernel: " + s.Text())
124 if strings.Contains(s.Text(), "Monogon BMaaS Agent started") {
125 agentStarted <- struct{}{}
126 break
127 }
128 }
129 qemuCmd.Wait()
130 }()
131 qemuCmd.Stderr = os.Stderr
132 sshPortCloser.Close()
133 if err := qemuCmd.Start(); err != nil {
134 t.Fatal(err)
135 }
136 defer qemuCmd.Process.Kill()
137
138 var c *ssh.Client
139 for {
140 c, err = ssh.Dial("tcp", net.JoinHostPort("localhost", fmt.Sprintf("%d", sshPort)), &ssh.ClientConfig{
141 User: "debian",
142 Auth: []ssh.AuthMethod{ssh.PublicKeys(sshPrivkey)},
143 HostKeyCallback: ssh.InsecureIgnoreHostKey(),
144 Timeout: 5 * time.Second,
145 })
146 if err != nil {
147 t.Logf("error connecting via SSH, retrying: %v", err)
148 time.Sleep(1 * time.Second)
149 continue
150 }
151 break
152 }
153 defer c.Close()
154 sc, err := sftp.NewClient(c)
155 if err != nil {
156 t.Fatal(err)
157 }
158 defer sc.Close()
159 takeoverFile, err := sc.Create("takeover")
160 if err != nil {
161 t.Fatal(err)
162 }
163 defer takeoverFile.Close()
164 if err := takeoverFile.Chmod(0o755); err != nil {
165 t.Fatal(err)
166 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000167 takeoverSrcFile, err := os.Open(xTakeoverPath)
Lorenz Brun2d284b52023-03-08 17:05:12 +0100168 if err != nil {
169 t.Fatal(err)
170 }
171 defer takeoverSrcFile.Close()
Tim Windelschmidt681d5152025-01-08 00:19:33 +0100172
Lorenz Brun2d284b52023-03-08 17:05:12 +0100173 if _, err := io.Copy(takeoverFile, takeoverSrcFile); err != nil {
174 t.Fatal(err)
175 }
176 if err := takeoverFile.Close(); err != nil {
177 t.Fatal(err)
178 }
179 sc.Close()
180
181 sess, err := c.NewSession()
182 if err != nil {
183 t.Fatal(err)
184 }
185 defer sess.Close()
186
187 init := api.TakeoverInit{
Lorenz Brun5b8b8602023-03-09 17:22:21 +0100188 MachineId: "test",
Lorenz Brun2d284b52023-03-08 17:05:12 +0100189 BmaasEndpoint: "localhost:1234",
190 }
191 initRaw, err := proto.Marshal(&init)
192 if err != nil {
193 t.Fatal(err)
194 }
195 sess.Stdin = bytes.NewReader(initRaw)
196 var stdoutBuf bytes.Buffer
197 var stderrBuf bytes.Buffer
198 sess.Stdout = &stdoutBuf
199 sess.Stderr = &stderrBuf
200 if err := sess.Run("sudo ./takeover"); err != nil {
201 t.Errorf("stderr:\n%s\n\n", stderrBuf.String())
202 t.Fatal(err)
203 }
204 var resp api.TakeoverResponse
205 if err := proto.Unmarshal(stdoutBuf.Bytes(), &resp); err != nil {
206 t.Fatal(err)
207 }
208 switch res := resp.Result.(type) {
209 case *api.TakeoverResponse_Success:
210 if res.Success.InitMessage.BmaasEndpoint != init.BmaasEndpoint {
Tim Windelschmidtd0d5d9d2025-03-26 22:07:11 +0100211 t.Fatal("InitMessage not passed through properly")
Lorenz Brun2d284b52023-03-08 17:05:12 +0100212 }
213 case *api.TakeoverResponse_Error:
214 t.Fatalf("takeover returned error: %v", res.Error.Message)
215 }
216 select {
217 case <-agentStarted:
218 // Done, test passed
219 case <-time.After(30 * time.Second):
220 t.Fatal("Waiting for BMaaS agent startup timed out")
221 }
222}