blob: e243d58f3062bc182bdd3597f462c49af3d71fd3 [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
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020041 xQEMUPath string
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000042)
43
44func init() {
45 var err error
46 for _, path := range []*string{
47 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +020048 &xTakeoverPath, &xQEMUPath,
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000049 } {
50 *path, err = runfiles.Rlocation(*path)
51 if err != nil {
52 panic(err)
53 }
54 }
55}
56
Lorenz Brun2d284b52023-03-08 17:05:12 +010057func 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 := ssh.NewPublicKey(pubKey)
64 if err != nil {
65 t.Fatal(err)
66 }
67
68 sshPrivkey, err := ssh.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(ssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
77 }
78
79 userData, err := json.Marshal(cloudConfig)
80 if err != nil {
81 t.Fatal(err)
82 }
83
Jan Schärc1b6df42025-03-20 08:52:18 +000084 root := structfs.Tree{
85 structfs.File("user-data", structfs.Bytes("#cloud-config\n"+string(userData))),
86 structfs.File("meta-data", structfs.Bytes("")),
Lorenz Brun2d284b52023-03-08 17:05:12 +010087 }
88 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
89 if err != nil {
90 t.Fatal(err)
91 }
92 defer os.Remove(cloudInitDataFile.Name())
Jan Schärc1b6df42025-03-20 08:52:18 +000093 if err := fat32.WriteFS(cloudInitDataFile, root, fat32.Options{Label: "cidata"}); err != nil {
Lorenz Brun2d284b52023-03-08 17:05:12 +010094 t.Fatal(err)
95 }
Lorenz Brun2d284b52023-03-08 17:05:12 +010096
97 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
98 if err != nil {
99 t.Fatal(err)
100 }
101
102 qemuArgs := []string{
103 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
104 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000105 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
106 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
107 "-drive", "if=virtio,format=qcow2,snapshot=on,cache=unsafe,file=" + xCloudImagePath,
Lorenz Brun2d284b52023-03-08 17:05:12 +0100108 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
109 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
110 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
111 "-device", "virtio-rng-pci",
112 "-serial", "stdio",
113 "-no-reboot",
114 }
Tim Windelschmidt8f1efe92025-04-01 01:28:43 +0200115 qemuCmd := exec.Command(xQEMUPath, qemuArgs...)
Lorenz Brun2d284b52023-03-08 17:05:12 +0100116 stdoutPipe, err := qemuCmd.StdoutPipe()
117 if err != nil {
118 t.Fatal(err)
119 }
120 agentStarted := make(chan struct{})
121 go func() {
122 s := bufio.NewScanner(stdoutPipe)
123 for s.Scan() {
124 t.Log("kernel: " + s.Text())
125 if strings.Contains(s.Text(), "Monogon BMaaS Agent started") {
126 agentStarted <- struct{}{}
127 break
128 }
129 }
130 qemuCmd.Wait()
131 }()
132 qemuCmd.Stderr = os.Stderr
133 sshPortCloser.Close()
134 if err := qemuCmd.Start(); err != nil {
135 t.Fatal(err)
136 }
137 defer qemuCmd.Process.Kill()
138
139 var c *ssh.Client
140 for {
141 c, err = ssh.Dial("tcp", net.JoinHostPort("localhost", fmt.Sprintf("%d", sshPort)), &ssh.ClientConfig{
142 User: "debian",
143 Auth: []ssh.AuthMethod{ssh.PublicKeys(sshPrivkey)},
144 HostKeyCallback: ssh.InsecureIgnoreHostKey(),
145 Timeout: 5 * time.Second,
146 })
147 if err != nil {
148 t.Logf("error connecting via SSH, retrying: %v", err)
149 time.Sleep(1 * time.Second)
150 continue
151 }
152 break
153 }
154 defer c.Close()
155 sc, err := sftp.NewClient(c)
156 if err != nil {
157 t.Fatal(err)
158 }
159 defer sc.Close()
160 takeoverFile, err := sc.Create("takeover")
161 if err != nil {
162 t.Fatal(err)
163 }
164 defer takeoverFile.Close()
165 if err := takeoverFile.Chmod(0o755); err != nil {
166 t.Fatal(err)
167 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000168 takeoverSrcFile, err := os.Open(xTakeoverPath)
Lorenz Brun2d284b52023-03-08 17:05:12 +0100169 if err != nil {
170 t.Fatal(err)
171 }
172 defer takeoverSrcFile.Close()
Tim Windelschmidt681d5152025-01-08 00:19:33 +0100173
Lorenz Brun2d284b52023-03-08 17:05:12 +0100174 if _, err := io.Copy(takeoverFile, takeoverSrcFile); err != nil {
175 t.Fatal(err)
176 }
177 if err := takeoverFile.Close(); err != nil {
178 t.Fatal(err)
179 }
180 sc.Close()
181
182 sess, err := c.NewSession()
183 if err != nil {
184 t.Fatal(err)
185 }
186 defer sess.Close()
187
188 init := api.TakeoverInit{
Lorenz Brun5b8b8602023-03-09 17:22:21 +0100189 MachineId: "test",
Lorenz Brun2d284b52023-03-08 17:05:12 +0100190 BmaasEndpoint: "localhost:1234",
191 }
192 initRaw, err := proto.Marshal(&init)
193 if err != nil {
194 t.Fatal(err)
195 }
196 sess.Stdin = bytes.NewReader(initRaw)
197 var stdoutBuf bytes.Buffer
198 var stderrBuf bytes.Buffer
199 sess.Stdout = &stdoutBuf
200 sess.Stderr = &stderrBuf
201 if err := sess.Run("sudo ./takeover"); err != nil {
202 t.Errorf("stderr:\n%s\n\n", stderrBuf.String())
203 t.Fatal(err)
204 }
205 var resp api.TakeoverResponse
206 if err := proto.Unmarshal(stdoutBuf.Bytes(), &resp); err != nil {
207 t.Fatal(err)
208 }
209 switch res := resp.Result.(type) {
210 case *api.TakeoverResponse_Success:
211 if res.Success.InitMessage.BmaasEndpoint != init.BmaasEndpoint {
Tim Windelschmidtd0d5d9d2025-03-26 22:07:11 +0100212 t.Fatal("InitMessage not passed through properly")
Lorenz Brun2d284b52023-03-08 17:05:12 +0100213 }
214 case *api.TakeoverResponse_Error:
215 t.Fatalf("takeover returned error: %v", res.Error.Message)
216 }
217 select {
218 case <-agentStarted:
219 // Done, test passed
220 case <-time.After(30 * time.Second):
221 t.Fatal("Waiting for BMaaS agent startup timed out")
222 }
223}