blob: 6d489eb1ba0aeccf3bf39d7d6147895a35451b35 [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
Lorenz Brun2d284b52023-03-08 17:05:12 +010025 "source.monogon.dev/metropolis/pkg/fat32"
26 "source.monogon.dev/metropolis/pkg/freeport"
27)
28
29func TestE2E(t *testing.T) {
30 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
31 if err != nil {
32 t.Fatal(err)
33 }
34
35 sshPubKey, err := ssh.NewPublicKey(pubKey)
36 if err != nil {
37 t.Fatal(err)
38 }
39
40 sshPrivkey, err := ssh.NewSignerFromKey(privKey)
41 if err != nil {
42 t.Fatal(err)
43 }
44
45 // CloudConfig doesn't really have a rigid spec, so just put things into it
46 cloudConfig := make(map[string]any)
47 cloudConfig["ssh_authorized_keys"] = []string{
48 strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(sshPubKey)), "\n"),
49 }
50
51 userData, err := json.Marshal(cloudConfig)
52 if err != nil {
53 t.Fatal(err)
54 }
55
56 rootInode := fat32.Inode{
57 Attrs: fat32.AttrDirectory,
58 Children: []*fat32.Inode{
59 {
60 Name: "user-data",
61 Content: strings.NewReader("#cloud-config\n" + string(userData)),
62 },
63 {
64 Name: "meta-data",
65 Content: strings.NewReader(""),
66 },
67 },
68 }
69 cloudInitDataFile, err := os.CreateTemp("", "cidata*.img")
70 if err != nil {
71 t.Fatal(err)
72 }
73 defer os.Remove(cloudInitDataFile.Name())
74 if err := fat32.WriteFS(cloudInitDataFile, rootInode, fat32.Options{Label: "cidata"}); err != nil {
75 t.Fatal(err)
76 }
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010077 cloudImagePath, err := runfiles.Rlocation("debian_11_cloudimage/file/downloaded")
Lorenz Brun2d284b52023-03-08 17:05:12 +010078 if err != nil {
79 t.Fatal(err)
80 }
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010081 ovmfVarsPath, err := runfiles.Rlocation("edk2/OVMF_VARS.fd")
Lorenz Brun2d284b52023-03-08 17:05:12 +010082 if err != nil {
83 t.Fatal(err)
84 }
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010085 ovmfCodePath, err := runfiles.Rlocation("edk2/OVMF_CODE.fd")
Lorenz Brun2d284b52023-03-08 17:05:12 +010086 if err != nil {
87 t.Fatal(err)
88 }
89
90 sshPort, sshPortCloser, err := freeport.AllocateTCPPort()
91 if err != nil {
92 t.Fatal(err)
93 }
94
95 qemuArgs := []string{
96 "-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults", "-m", "1024",
97 "-cpu", "host", "-smp", "sockets=1,cpus=1,cores=2,threads=2,maxcpus=4",
Tim Windelschmidtb40b9182024-02-06 07:08:35 +010098 "-drive", "if=pflash,format=raw,readonly=on,file=" + ovmfCodePath,
Lorenz Brun2d284b52023-03-08 17:05:12 +010099 "-drive", "if=pflash,format=raw,snapshot=on,file=" + ovmfVarsPath,
100 "-drive", "if=virtio,format=qcow2,snapshot=on,cache=unsafe,file=" + cloudImagePath,
101 "-drive", "if=virtio,format=raw,snapshot=on,file=" + cloudInitDataFile.Name(),
102 "-netdev", fmt.Sprintf("user,id=net0,net=10.42.0.0/24,dhcpstart=10.42.0.10,hostfwd=tcp::%d-:22", sshPort),
103 "-device", "virtio-net-pci,netdev=net0,mac=22:d5:8e:76:1d:07",
104 "-device", "virtio-rng-pci",
105 "-serial", "stdio",
106 "-no-reboot",
107 }
108 qemuCmd := exec.Command("qemu-system-x86_64", qemuArgs...)
109 stdoutPipe, err := qemuCmd.StdoutPipe()
110 if err != nil {
111 t.Fatal(err)
112 }
113 agentStarted := make(chan struct{})
114 go func() {
115 s := bufio.NewScanner(stdoutPipe)
116 for s.Scan() {
117 t.Log("kernel: " + s.Text())
118 if strings.Contains(s.Text(), "Monogon BMaaS Agent started") {
119 agentStarted <- struct{}{}
120 break
121 }
122 }
123 qemuCmd.Wait()
124 }()
125 qemuCmd.Stderr = os.Stderr
126 sshPortCloser.Close()
127 if err := qemuCmd.Start(); err != nil {
128 t.Fatal(err)
129 }
130 defer qemuCmd.Process.Kill()
131
132 var c *ssh.Client
133 for {
134 c, err = ssh.Dial("tcp", net.JoinHostPort("localhost", fmt.Sprintf("%d", sshPort)), &ssh.ClientConfig{
135 User: "debian",
136 Auth: []ssh.AuthMethod{ssh.PublicKeys(sshPrivkey)},
137 HostKeyCallback: ssh.InsecureIgnoreHostKey(),
138 Timeout: 5 * time.Second,
139 })
140 if err != nil {
141 t.Logf("error connecting via SSH, retrying: %v", err)
142 time.Sleep(1 * time.Second)
143 continue
144 }
145 break
146 }
147 defer c.Close()
148 sc, err := sftp.NewClient(c)
149 if err != nil {
150 t.Fatal(err)
151 }
152 defer sc.Close()
153 takeoverFile, err := sc.Create("takeover")
154 if err != nil {
155 t.Fatal(err)
156 }
157 defer takeoverFile.Close()
158 if err := takeoverFile.Chmod(0o755); err != nil {
159 t.Fatal(err)
160 }
Tim Windelschmidt79ffbbe2024-02-22 19:15:51 +0100161 takeoverPath, err := runfiles.Rlocation("_main/cloud/agent/takeover/takeover_/takeover")
Lorenz Brun2d284b52023-03-08 17:05:12 +0100162 if err != nil {
163 t.Fatal(err)
164 }
165 takeoverSrcFile, err := os.Open(takeoverPath)
166 if err != nil {
167 t.Fatal(err)
168 }
169 defer takeoverSrcFile.Close()
170 if _, err := io.Copy(takeoverFile, takeoverSrcFile); err != nil {
171 t.Fatal(err)
172 }
173 if err := takeoverFile.Close(); err != nil {
174 t.Fatal(err)
175 }
176 sc.Close()
177
178 sess, err := c.NewSession()
179 if err != nil {
180 t.Fatal(err)
181 }
182 defer sess.Close()
183
184 init := api.TakeoverInit{
Lorenz Brun5b8b8602023-03-09 17:22:21 +0100185 MachineId: "test",
Lorenz Brun2d284b52023-03-08 17:05:12 +0100186 BmaasEndpoint: "localhost:1234",
187 }
188 initRaw, err := proto.Marshal(&init)
189 if err != nil {
190 t.Fatal(err)
191 }
192 sess.Stdin = bytes.NewReader(initRaw)
193 var stdoutBuf bytes.Buffer
194 var stderrBuf bytes.Buffer
195 sess.Stdout = &stdoutBuf
196 sess.Stderr = &stderrBuf
197 if err := sess.Run("sudo ./takeover"); err != nil {
198 t.Errorf("stderr:\n%s\n\n", stderrBuf.String())
199 t.Fatal(err)
200 }
201 var resp api.TakeoverResponse
202 if err := proto.Unmarshal(stdoutBuf.Bytes(), &resp); err != nil {
203 t.Fatal(err)
204 }
205 switch res := resp.Result.(type) {
206 case *api.TakeoverResponse_Success:
207 if res.Success.InitMessage.BmaasEndpoint != init.BmaasEndpoint {
208 t.Error("InitMessage not passed through properly")
209 }
210 case *api.TakeoverResponse_Error:
211 t.Fatalf("takeover returned error: %v", res.Error.Message)
212 }
213 select {
214 case <-agentStarted:
215 // Done, test passed
216 case <-time.After(30 * time.Second):
217 t.Fatal("Waiting for BMaaS agent startup timed out")
218 }
219}