blob: 9ab6769a0d948e150e46d063aafb554fc0aa00dd [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"
Lorenz Brun2d284b52023-03-08 17:05:12 +010030)
31
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000032var (
33 // These are filled by bazel at linking time with the canonical path of
34 // their corresponding file. Inside the init function we resolve it
35 // with the rules_go runfiles package to the real path.
36 xCloudImagePath string
37 xOvmfVarsPath string
38 xOvmfCodePath string
39 xTakeoverPath string
40)
41
42func init() {
43 var err error
44 for _, path := range []*string{
45 &xCloudImagePath, &xOvmfVarsPath, &xOvmfCodePath,
46 &xTakeoverPath,
47 } {
48 *path, err = runfiles.Rlocation(*path)
49 if err != nil {
50 panic(err)
51 }
52 }
53}
54
Lorenz Brun2d284b52023-03-08 17:05:12 +010055func TestE2E(t *testing.T) {
56 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
57 if err != nil {
58 t.Fatal(err)
59 }
60
61 sshPubKey, err := ssh.NewPublicKey(pubKey)
62 if err != nil {
63 t.Fatal(err)
64 }
65
66 sshPrivkey, err := ssh.NewSignerFromKey(privKey)
67 if err != nil {
68 t.Fatal(err)
69 }
70
71 // CloudConfig doesn't really have a rigid spec, so just put things into it
72 cloudConfig := make(map[string]any)
73 cloudConfig["ssh_authorized_keys"] = []string{
74 strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
75 }
76
77 userData, err := json.Marshal(cloudConfig)
78 if err != nil {
79 t.Fatal(err)
80 }
81
82 rootInode := fat32.Inode{
83 Attrs: fat32.AttrDirectory,
84 Children: []*fat32.Inode{
85 {
86 Name: "user-data",
87 Content: strings.NewReader("#cloud-config\n" + string(userData)),
88 },
89 {
90 Name: "meta-data",
91 Content: strings.NewReader(""),
92 },
93 },
94 }
95 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
96 if err != nil {
97 t.Fatal(err)
98 }
99 defer os.Remove(cloudInitDataFile.Name())
100 if err := fat32.WriteFS(cloudInitDataFile, rootInode, fat32.Options{Label: "cidata"}); err != nil {
101 t.Fatal(err)
102 }
Lorenz Brun2d284b52023-03-08 17:05:12 +0100103
104 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
105 if err != nil {
106 t.Fatal(err)
107 }
108
109 qemuArgs := []string{
110 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
111 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000112 "-drive", "if=pflash,format=raw,readonly=on,file=" + xOvmfCodePath,
113 "-drive", "if=pflash,format=raw,snapshot=on,file=" + xOvmfVarsPath,
114 "-drive", "if=virtio,format=qcow2,snapshot=on,cache=unsafe,file=" + xCloudImagePath,
Lorenz Brun2d284b52023-03-08 17:05:12 +0100115 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
116 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
117 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
118 "-device", "virtio-rng-pci",
119 "-serial", "stdio",
120 "-no-reboot",
121 }
122 qemuCmd := exec.Command("qemu-system-x86_64", qemuArgs...)
123 stdoutPipe, err := qemuCmd.StdoutPipe()
124 if err != nil {
125 t.Fatal(err)
126 }
127 agentStarted := make(chan struct{})
128 go func() {
129 s := bufio.NewScanner(stdoutPipe)
130 for s.Scan() {
131 t.Log("kernel: " + s.Text())
132 if strings.Contains(s.Text(), "Monogon BMaaS Agent started") {
133 agentStarted <- struct{}{}
134 break
135 }
136 }
137 qemuCmd.Wait()
138 }()
139 qemuCmd.Stderr = os.Stderr
140 sshPortCloser.Close()
141 if err := qemuCmd.Start(); err != nil {
142 t.Fatal(err)
143 }
144 defer qemuCmd.Process.Kill()
145
146 var c *ssh.Client
147 for {
148 c, err = ssh.Dial("tcp", net.JoinHostPort("localhost", fmt.Sprintf("%d", sshPort)), &ssh.ClientConfig{
149 User: "debian",
150 Auth: []ssh.AuthMethod{ssh.PublicKeys(sshPrivkey)},
151 HostKeyCallback: ssh.InsecureIgnoreHostKey(),
152 Timeout: 5 * time.Second,
153 })
154 if err != nil {
155 t.Logf("error connecting via SSH, retrying: %v", err)
156 time.Sleep(1 * time.Second)
157 continue
158 }
159 break
160 }
161 defer c.Close()
162 sc, err := sftp.NewClient(c)
163 if err != nil {
164 t.Fatal(err)
165 }
166 defer sc.Close()
167 takeoverFile, err := sc.Create("takeover")
168 if err != nil {
169 t.Fatal(err)
170 }
171 defer takeoverFile.Close()
172 if err := takeoverFile.Chmod(0o755); err != nil {
173 t.Fatal(err)
174 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000175 takeoverSrcFile, err := os.Open(xTakeoverPath)
Lorenz Brun2d284b52023-03-08 17:05:12 +0100176 if err != nil {
177 t.Fatal(err)
178 }
179 defer takeoverSrcFile.Close()
Tim Windelschmidt681d5152025-01-08 00:19:33 +0100180
Lorenz Brun2d284b52023-03-08 17:05:12 +0100181 if _, err := io.Copy(takeoverFile, takeoverSrcFile); err != nil {
182 t.Fatal(err)
183 }
184 if err := takeoverFile.Close(); err != nil {
185 t.Fatal(err)
186 }
187 sc.Close()
188
189 sess, err := c.NewSession()
190 if err != nil {
191 t.Fatal(err)
192 }
193 defer sess.Close()
194
195 init := api.TakeoverInit{
Lorenz Brun5b8b8602023-03-09 17:22:21 +0100196 MachineId: "test",
Lorenz Brun2d284b52023-03-08 17:05:12 +0100197 BmaasEndpoint: "localhost:1234",
198 }
199 initRaw, err := proto.Marshal(&init)
200 if err != nil {
201 t.Fatal(err)
202 }
203 sess.Stdin = bytes.NewReader(initRaw)
204 var stdoutBuf bytes.Buffer
205 var stderrBuf bytes.Buffer
206 sess.Stdout = &stdoutBuf
207 sess.Stderr = &stderrBuf
208 if err := sess.Run("sudo ./takeover"); err != nil {
209 t.Errorf("stderr:\n%s\n\n", stderrBuf.String())
210 t.Fatal(err)
211 }
212 var resp api.TakeoverResponse
213 if err := proto.Unmarshal(stdoutBuf.Bytes(), &resp); err != nil {
214 t.Fatal(err)
215 }
216 switch res := resp.Result.(type) {
217 case *api.TakeoverResponse_Success:
218 if res.Success.InitMessage.BmaasEndpoint != init.BmaasEndpoint {
219 t.Error("InitMessage not passed through properly")
220 }
221 case *api.TakeoverResponse_Error:
222 t.Fatalf("takeover returned error: %v", res.Error.Message)
223 }
224 select {
225 case <-agentStarted:
226 // Done, test passed
227 case <-time.After(30 * time.Second):
228 t.Fatal("Waiting for BMaaS agent startup timed out")
229 }
230}