v0.4.3 bdc41cd

api.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close