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