blob: 0521cc944fb3f3ff2f9bb38e8fa34aa827380eff [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()
177 if _, err := io.Copy(takeoverFile, takeoverSrcFile); err != nil {
178 t.Fatal(err)
179 }
180 if err := takeoverFile.Close(); err != nil {
181 t.Fatal(err)
182 }
183 sc.Close()
184
185 sess, err := c.NewSession()
186 if err != nil {
187 t.Fatal(err)
188 }
189 defer sess.Close()
190
191 init := api.TakeoverInit{
Lorenz Brun5b8b8602023-03-09 17:22:21 +0100192 MachineId: "test",
Lorenz Brun2d284b52023-03-08 17:05:12 +0100193 BmaasEndpoint: "localhost:1234",
194 }
195 initRaw, err := proto.Marshal(&init)
196 if err != nil {
197 t.Fatal(err)
198 }
199 sess.Stdin = bytes.NewReader(initRaw)
200 var stdoutBuf bytes.Buffer
201 var stderrBuf bytes.Buffer
202 sess.Stdout = &stdoutBuf
203 sess.Stderr = &stderrBuf
204 if err := sess.Run("sudo ./takeover"); err != nil {
205 t.Errorf("stderr:\n%s\n\n", stderrBuf.String())
206 t.Fatal(err)
207 }
208 var resp api.TakeoverResponse
209 if err := proto.Unmarshal(stdoutBuf.Bytes(), &resp); err != nil {
210 t.Fatal(err)
211 }
212 switch res := resp.Result.(type) {
213 case *api.TakeoverResponse_Success:
214 if res.Success.InitMessage.BmaasEndpoint != init.BmaasEndpoint {
215 t.Error("InitMessage not passed through properly")
216 }
217 case *api.TakeoverResponse_Error:
218 t.Fatalf("takeover returned error: %v", res.Error.Message)
219 }
220 select {
221 case <-agentStarted:
222 // Done, test passed
223 case <-time.After(30 * time.Second):
224 t.Fatal("Waiting for BMaaS agent startup timed out")
225 }
226}