1// SPDX-License-Identifier: MIT
2
3package vm
4
5import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "fmt"
10 "io"
11 "net"
12 "net/http"
13 "time"
14)
15
16// Firecracker is controlled through its HTTP API on a unix socket, with
17// net/http, rather than through firecracker-go-sdk: the SDK's last release
18// (v1.0.0, 2022) predates Firecracker 1.17 and drags in CNI, go-openapi and
19// netlink for what is a handful of PUT requests. The structures below are the
20// subset of the API this driver uses; Firecracker rejects unknown or invalid
21// fields with a fault message, which surfaces in the returned error.
22
23type machineConfig struct {
24 VcpuCount int `json:"vcpu_count"`
25 MemSizeMiB int `json:"mem_size_mib"`
26 SMT bool `json:"smt"`
27}
28
29type bootSource struct {
30 KernelImagePath string `json:"kernel_image_path"`
31 BootArgs string `json:"boot_args"`
32}
33
34type drive struct {
35 DriveID string `json:"drive_id"`
36 PathOnHost string `json:"path_on_host"`
37 IsRootDevice bool `json:"is_root_device"`
38 IsReadOnly bool `json:"is_read_only"`
39 RateLimiter *rateLimiter `json:"rate_limiter,omitempty"`
40}
41
42type networkInterface struct {
43 IfaceID string `json:"iface_id"`
44 HostDevName string `json:"host_dev_name"`
45 GuestMAC string `json:"guest_mac"`
46 RxRateLimiter *rateLimiter `json:"rx_rate_limiter,omitempty"`
47 TxRateLimiter *rateLimiter `json:"tx_rate_limiter,omitempty"`
48}
49
50// tokenBucket is Firecracker's rate limiter bucket: Size tokens (bytes or
51// operations) are refilled every RefillTime milliseconds.
52type tokenBucket struct {
53 Size int64 `json:"size"`
54 OneTimeBurst int64 `json:"one_time_burst,omitempty"`
55 RefillTime int64 `json:"refill_time"`
56}
57
58type rateLimiter struct {
59 Bandwidth *tokenBucket `json:"bandwidth,omitempty"`
60 Ops *tokenBucket `json:"ops,omitempty"`
61}
62
63// diskLimiter limits a disk to bandwidthMiB MiB/s and iops operations/s (zero
64// disables a limit; both zero means no limiter). A one-second burst is allowed
65// at the start so that short bursts (a git checkout) are not slowed down.
66func diskLimiter(bandwidthMiB, iops int) *rateLimiter {
67 var rl rateLimiter
68 if bandwidthMiB > 0 {
69 b := int64(bandwidthMiB) << 20
70 rl.Bandwidth = &tokenBucket{Size: b, OneTimeBurst: b, RefillTime: 1000}
71 }
72 if iops > 0 {
73 rl.Ops = &tokenBucket{Size: int64(iops), OneTimeBurst: int64(iops), RefillTime: 1000}
74 }
75 if rl.Bandwidth == nil && rl.Ops == nil {
76 return nil
77 }
78 return &rl
79}
80
81// netLimiter limits one direction of a network interface to mbit Mbit/s.
82func netLimiter(mbit int) *rateLimiter {
83 if mbit <= 0 {
84 return nil
85 }
86 b := int64(mbit) * 1_000_000 / 8
87 return &rateLimiter{Bandwidth: &tokenBucket{Size: b, OneTimeBurst: b, RefillTime: 1000}}
88}
89
90type vsockDevice struct {
91 GuestCID uint32 `json:"guest_cid"`
92 UDSPath string `json:"uds_path"`
93}
94
95type action struct {
96 ActionType string `json:"action_type"`
97}
98
99// apiClient talks to one Firecracker instance.
100type apiClient struct {
101 http *http.Client
102}
103
104func newAPIClient(socket string) *apiClient {
105 return &apiClient{http: &http.Client{
106 Timeout: 10 * time.Second,
107 Transport: &http.Transport{
108 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
109 var d net.Dialer
110 return d.DialContext(ctx, "unix", socket)
111 },
112 DisableKeepAlives: true,
113 },
114 }}
115}
116
117// put sends a PUT with a JSON body and expects 204 (or 200).
118func (c *apiClient) put(ctx context.Context, path string, body any) error {
119 b, err := json.Marshal(body)
120 if err != nil {
121 return err
122 }
123 req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://firecracker"+path, bytes.NewReader(b))
124 if err != nil {
125 return err
126 }
127 req.Header.Set("Content-Type", "application/json")
128 resp, err := c.http.Do(req)
129 if err != nil {
130 return fmt.Errorf("firecracker PUT %s: %w", path, err)
131 }
132 defer func() { _ = resp.Body.Close() }()
133 if resp.StatusCode/100 != 2 {
134 msg, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
135 return fmt.Errorf("firecracker PUT %s: %s: %s", path, resp.Status, bytes.TrimSpace(msg))
136 }
137 return nil
138}
139
140// ready reports whether the API answers GET /.
141func (c *apiClient) ready(ctx context.Context) error {
142 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://firecracker/", nil)
143 if err != nil {
144 return err
145 }
146 resp, err := c.http.Do(req)
147 if err != nil {
148 return err
149 }
150 defer func() { _ = resp.Body.Close() }()
151 _, _ = io.Copy(io.Discard, resp.Body)
152 if resp.StatusCode/100 != 2 {
153 return fmt.Errorf("firecracker GET /: %s", resp.Status)
154 }
155 return nil
156}
157
158// vmConfig is everything configured before boot.
159type vmConfig struct {
160 Machine machineConfig
161 Boot bootSource
162 Drives []drive
163 Net networkInterface
164 Vsock vsockDevice
165}
166
167// configure applies the pre-boot configuration and boots the VM. The order is
168// fixed (and covered by a unit test) because Firecracker rejects some changes
169// after boot and drive order decides the guest device names vda, vdb, vdc.
170func (c *apiClient) configure(ctx context.Context, cfg vmConfig) error {
171 steps := []struct {
172 path string
173 body any
174 }{
175 {"/machine-config", cfg.Machine},
176 {"/boot-source", cfg.Boot},
177 }
178 for _, d := range cfg.Drives {
179 steps = append(steps, struct {
180 path string
181 body any
182 }{"/drives/" + d.DriveID, d})
183 }
184 steps = append(steps,
185 struct {
186 path string
187 body any
188 }{"/network-interfaces/" + cfg.Net.IfaceID, cfg.Net},
189 struct {
190 path string
191 body any
192 }{"/vsock", cfg.Vsock},
193 struct {
194 path string
195 body any
196 }{"/entropy", struct{}{}},
197 struct {
198 path string
199 body any
200 }{"/actions", action{ActionType: "InstanceStart"}},
201 )
202 for _, s := range steps {
203 if err := c.put(ctx, s.path, s.body); err != nil {
204 return err
205 }
206 }
207 return nil
208}