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