- Replace reflink rootfs copy with device-mapper snapshots (shared read-only loop device per base template, per-sandbox sparse CoW file) - Add devicemapper package with create/restore/remove/flatten operations and refcounted LoopRegistry for base image loop devices - Fix pause ordering: destroy VM before removing dm-snapshot to avoid "device busy" error (FC must release the dm device first) - Add test UI at GET /test for sandbox lifecycle management (create, pause, resume, destroy, exec, snapshot create/list/delete) - Fix DirSize to report actual disk usage (stat.Blocks * 512) instead of apparent size, so sparse CoW files report correctly - Add timing logs to pause flow for performance diagnostics - Fix all lint errors across api, network, vm, uffd, and sandbox packages - Remove obsolete internal/filesystem package (replaced by devicemapper) - Update CLAUDE.md with device-mapper architecture documentation
147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
package vm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// fcClient talks to the Firecracker HTTP API over a Unix socket.
|
|
type fcClient struct {
|
|
http *http.Client
|
|
socketPath string
|
|
}
|
|
|
|
func newFCClient(socketPath string) *fcClient {
|
|
return &fcClient{
|
|
socketPath: socketPath,
|
|
http: &http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
var d net.Dialer
|
|
return d.DialContext(ctx, "unix", socketPath)
|
|
},
|
|
},
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *fcClient) do(ctx context.Context, method, path string, body any) error {
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal request body: %w", err)
|
|
}
|
|
bodyReader = bytes.NewReader(data)
|
|
}
|
|
|
|
// The host in the URL is ignored for Unix sockets; we use "localhost" by convention.
|
|
req, err := http.NewRequestWithContext(ctx, method, "http://localhost"+path, bodyReader)
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("%s %s: %w", method, path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("%s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setBootSource configures the kernel and boot args.
|
|
func (c *fcClient) setBootSource(ctx context.Context, kernelPath, bootArgs string) error {
|
|
return c.do(ctx, http.MethodPut, "/boot-source", map[string]string{
|
|
"kernel_image_path": kernelPath,
|
|
"boot_args": bootArgs,
|
|
})
|
|
}
|
|
|
|
// setRootfsDrive configures the root filesystem drive.
|
|
func (c *fcClient) setRootfsDrive(ctx context.Context, driveID, path string, readOnly bool) error {
|
|
return c.do(ctx, http.MethodPut, "/drives/"+driveID, map[string]any{
|
|
"drive_id": driveID,
|
|
"path_on_host": path,
|
|
"is_root_device": true,
|
|
"is_read_only": readOnly,
|
|
})
|
|
}
|
|
|
|
// setNetworkInterface configures a network interface attached to a TAP device.
|
|
func (c *fcClient) setNetworkInterface(ctx context.Context, ifaceID, tapName, macAddr string) error {
|
|
return c.do(ctx, http.MethodPut, "/network-interfaces/"+ifaceID, map[string]any{
|
|
"iface_id": ifaceID,
|
|
"host_dev_name": tapName,
|
|
"guest_mac": macAddr,
|
|
})
|
|
}
|
|
|
|
// setMachineConfig configures vCPUs, memory, and other machine settings.
|
|
func (c *fcClient) setMachineConfig(ctx context.Context, vcpus, memMB int) error {
|
|
return c.do(ctx, http.MethodPut, "/machine-config", map[string]any{
|
|
"vcpu_count": vcpus,
|
|
"mem_size_mib": memMB,
|
|
"smt": false,
|
|
})
|
|
}
|
|
|
|
// startVM issues the InstanceStart action.
|
|
func (c *fcClient) startVM(ctx context.Context) error {
|
|
return c.do(ctx, http.MethodPut, "/actions", map[string]string{
|
|
"action_type": "InstanceStart",
|
|
})
|
|
}
|
|
|
|
// pauseVM pauses the microVM.
|
|
func (c *fcClient) pauseVM(ctx context.Context) error {
|
|
return c.do(ctx, http.MethodPatch, "/vm", map[string]string{
|
|
"state": "Paused",
|
|
})
|
|
}
|
|
|
|
// resumeVM resumes a paused microVM.
|
|
func (c *fcClient) resumeVM(ctx context.Context) error {
|
|
return c.do(ctx, http.MethodPatch, "/vm", map[string]string{
|
|
"state": "Resumed",
|
|
})
|
|
}
|
|
|
|
// createSnapshot creates a full VM snapshot.
|
|
func (c *fcClient) createSnapshot(ctx context.Context, snapPath, memPath string) error {
|
|
return c.do(ctx, http.MethodPut, "/snapshot/create", map[string]any{
|
|
"snapshot_type": "Full",
|
|
"snapshot_path": snapPath,
|
|
"mem_file_path": memPath,
|
|
})
|
|
}
|
|
|
|
// loadSnapshotWithUffd loads a VM snapshot using a UFFD socket for
|
|
// lazy memory loading. Firecracker will connect to the socket and
|
|
// send the uffd fd + memory region mappings.
|
|
func (c *fcClient) loadSnapshotWithUffd(ctx context.Context, snapPath, uffdSocketPath string) error {
|
|
return c.do(ctx, http.MethodPut, "/snapshot/load", map[string]any{
|
|
"snapshot_path": snapPath,
|
|
"resume_vm": false,
|
|
"mem_backend": map[string]any{
|
|
"backend_type": "Uffd",
|
|
"backend_path": uffdSocketPath,
|
|
},
|
|
})
|
|
}
|