Add sandbox snapshot and restore with UFFD lazy memory loading

Implement full snapshot lifecycle: pause (snapshot + free resources),
resume (UFFD-based lazy restore), and named snapshot templates that
can spawn new sandboxes from frozen VM state.

Key changes:
- Snapshot header system with generational diff mapping (inspired by e2b)
- UFFD server for lazy page fault handling during snapshot restore
- Stable rootfs symlink path (/tmp/fc-vm/) for snapshot compatibility
- Templates DB table and CRUD API endpoints (POST/GET/DELETE /v1/snapshots)
- CreateSnapshot/DeleteSnapshot RPCs in hostagent proto
- Reconciler excludes paused sandboxes (expected absent from host agent)
- Snapshot templates lock vcpus/memory to baked-in values
- Proper cleanup of uffd sockets and pause snapshot files on destroy
This commit is contained in:
2026-03-12 09:19:37 +06:00
parent 9b94df7f56
commit a1bd439c75
33 changed files with 2714 additions and 166 deletions

View File

@ -10,6 +10,7 @@ AGENT_LISTEN_ADDR=:50051
AGENT_KERNEL_PATH=/var/lib/wrenn/kernels/vmlinux
AGENT_IMAGES_PATH=/var/lib/wrenn/images
AGENT_SANDBOXES_PATH=/var/lib/wrenn/sandboxes
AGENT_SNAPSHOTS_PATH=/var/lib/wrenn/snapshots
AGENT_HOST_INTERFACE=eth0
# Lago (billing — external service)

View File

@ -209,7 +209,7 @@ The main module (`go.mod`) and envd (`envd/go.mod`) are fully independent. `make
- Kernel: `/var/lib/wrenn/kernels/vmlinux`
- Base rootfs images: `/var/lib/wrenn/images/{template}.ext4`
- Sandbox clones: `/var/lib/wrenn/sandboxes/`
- Firecracker: `/usr/local/bin/firecracker`
- Firecracker: `/usr/local/bin/firecracker` (e2b's fork of firecracker)
## Web UI Styling

View File

@ -33,11 +33,13 @@ func main() {
kernelPath := envOrDefault("AGENT_KERNEL_PATH", "/var/lib/wrenn/kernels/vmlinux")
imagesPath := envOrDefault("AGENT_IMAGES_PATH", "/var/lib/wrenn/images")
sandboxesPath := envOrDefault("AGENT_SANDBOXES_PATH", "/var/lib/wrenn/sandboxes")
snapshotsPath := envOrDefault("AGENT_SNAPSHOTS_PATH", "/var/lib/wrenn/snapshots")
cfg := sandbox.Config{
KernelPath: kernelPath,
ImagesDir: imagesPath,
SandboxesDir: sandboxesPath,
SnapshotsDir: snapshotsPath,
}
mgr := sandbox.New(cfg)
@ -91,4 +93,3 @@ func envOrDefault(key, def string) string {
}
return def
}

View File

@ -0,0 +1,14 @@
-- +goose Up
CREATE TABLE templates (
name TEXT PRIMARY KEY,
type TEXT NOT NULL DEFAULT 'base', -- 'base' or 'snapshot'
vcpus INTEGER,
memory_mb INTEGER,
size_bytes BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- +goose Down
DROP TABLE templates;

16
db/queries/templates.sql Normal file
View File

@ -0,0 +1,16 @@
-- name: InsertTemplate :one
INSERT INTO templates (name, type, vcpus, memory_mb, size_bytes)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: GetTemplate :one
SELECT * FROM templates WHERE name = $1;
-- name: ListTemplates :many
SELECT * FROM templates ORDER BY created_at DESC;
-- name: ListTemplatesByType :many
SELECT * FROM templates WHERE type = $1 ORDER BY created_at DESC;
-- name: DeleteTemplate :exec
DELETE FROM templates WHERE name = $1;

View File

@ -16,9 +16,9 @@ import (
"github.com/stretchr/testify/require"
"git.omukk.dev/wrenn/sandbox/envd/internal/execcontext"
"git.omukk.dev/wrenn/sandbox/envd/internal/utils"
"git.omukk.dev/wrenn/sandbox/envd/internal/shared/keys"
utilsShared "git.omukk.dev/wrenn/sandbox/envd/internal/shared/utils"
"git.omukk.dev/wrenn/sandbox/envd/internal/utils"
)
func TestSimpleCases(t *testing.T) {

View File

@ -16,8 +16,8 @@ import (
"git.omukk.dev/wrenn/sandbox/envd/internal/logs"
"git.omukk.dev/wrenn/sandbox/envd/internal/permissions"
rpc "git.omukk.dev/wrenn/sandbox/envd/internal/services/spec/filesystem"
"git.omukk.dev/wrenn/sandbox/envd/internal/utils"
"git.omukk.dev/wrenn/sandbox/envd/internal/shared/id"
"git.omukk.dev/wrenn/sandbox/envd/internal/utils"
)
type FileWatcher struct {

4
go.mod
View File

@ -13,10 +13,12 @@ require (
)
require (
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.29.0 // indirect
)

6
go.sum
View File

@ -1,5 +1,7 @@
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -7,6 +9,8 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -35,6 +39,8 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@ -44,7 +44,7 @@ type wsStopMsg struct {
// wsOutMsg is sent by the server for process events.
type wsOutMsg struct {
Type string `json:"type"` // "start", "stdout", "stderr", "exit", "error"
Type string `json:"type"` // "start", "stdout", "stderr", "exit", "error"
PID uint32 `json:"pid,omitempty"` // only for "start"
Data string `json:"data,omitempty"` // only for "stdout", "stderr", "error"
ExitCode *int32 `json:"exit_code,omitempty"` // only for "exit"

View File

@ -97,6 +97,17 @@ func (h *sandboxHandler) Create(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
// If the template is a snapshot, use its baked-in vcpus/memory
// (they cannot be changed since the VM state is frozen).
if tmpl, err := h.db.GetTemplate(ctx, req.Template); err == nil && tmpl.Type == "snapshot" {
if tmpl.Vcpus.Valid {
req.VCPUs = tmpl.Vcpus.Int32
}
if tmpl.MemoryMb.Valid {
req.MemoryMB = tmpl.MemoryMb.Int32
}
}
sandboxID := id.NewSandboxID()
// Insert pending record.
@ -182,6 +193,8 @@ func (h *sandboxHandler) Get(w http.ResponseWriter, r *http.Request) {
}
// Pause handles POST /v1/sandboxes/{id}/pause.
// Pause = snapshot + destroy. The sandbox is frozen to disk and all running
// resources are released. It can be resumed later.
func (h *sandboxHandler) Pause(w http.ResponseWriter, r *http.Request) {
sandboxID := chi.URLParam(r, "id")
ctx := r.Context()
@ -216,6 +229,7 @@ func (h *sandboxHandler) Pause(w http.ResponseWriter, r *http.Request) {
}
// Resume handles POST /v1/sandboxes/{id}/resume.
// Resume restores a paused sandbox from snapshot using UFFD lazy memory loading.
func (h *sandboxHandler) Resume(w http.ResponseWriter, r *http.Request) {
sandboxID := chi.URLParam(r, "id")
ctx := r.Context()
@ -230,16 +244,24 @@ func (h *sandboxHandler) Resume(w http.ResponseWriter, r *http.Request) {
return
}
if _, err := h.agent.ResumeSandbox(ctx, connect.NewRequest(&pb.ResumeSandboxRequest{
resp, err := h.agent.ResumeSandbox(ctx, connect.NewRequest(&pb.ResumeSandboxRequest{
SandboxId: sandboxID,
})); err != nil {
}))
if err != nil {
status, code, msg := agentErrToHTTP(err)
writeError(w, status, code, msg)
return
}
sb, err = h.db.UpdateSandboxStatus(ctx, db.UpdateSandboxStatusParams{
ID: sandboxID, Status: "running",
now := time.Now()
sb, err = h.db.UpdateSandboxRunning(ctx, db.UpdateSandboxRunningParams{
ID: sandboxID,
HostIp: resp.Msg.HostIp,
GuestIp: "",
StartedAt: pgtype.Timestamptz{
Time: now,
Valid: true,
},
})
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error", "failed to update status")

View File

@ -0,0 +1,187 @@
package api
import (
"encoding/json"
"log/slog"
"net/http"
"time"
"connectrpc.com/connect"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgtype"
"git.omukk.dev/wrenn/sandbox/internal/db"
"git.omukk.dev/wrenn/sandbox/internal/id"
pb "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen"
"git.omukk.dev/wrenn/sandbox/proto/hostagent/gen/hostagentv1connect"
)
type snapshotHandler struct {
db *db.Queries
agent hostagentv1connect.HostAgentServiceClient
}
func newSnapshotHandler(db *db.Queries, agent hostagentv1connect.HostAgentServiceClient) *snapshotHandler {
return &snapshotHandler{db: db, agent: agent}
}
type createSnapshotRequest struct {
SandboxID string `json:"sandbox_id"`
Name string `json:"name"`
}
type snapshotResponse struct {
Name string `json:"name"`
Type string `json:"type"`
VCPUs *int32 `json:"vcpus,omitempty"`
MemoryMB *int32 `json:"memory_mb,omitempty"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt string `json:"created_at"`
}
func templateToResponse(t db.Template) snapshotResponse {
resp := snapshotResponse{
Name: t.Name,
Type: t.Type,
SizeBytes: t.SizeBytes,
}
if t.Vcpus.Valid {
resp.VCPUs = &t.Vcpus.Int32
}
if t.MemoryMb.Valid {
resp.MemoryMB = &t.MemoryMb.Int32
}
if t.CreatedAt.Valid {
resp.CreatedAt = t.CreatedAt.Time.Format(time.RFC3339)
}
return resp
}
// Create handles POST /v1/snapshots.
func (h *snapshotHandler) Create(w http.ResponseWriter, r *http.Request) {
var req createSnapshotRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
return
}
if req.SandboxID == "" {
writeError(w, http.StatusBadRequest, "invalid_request", "sandbox_id is required")
return
}
if req.Name == "" {
req.Name = id.NewSnapshotName()
}
ctx := r.Context()
overwrite := r.URL.Query().Get("overwrite") == "true"
// Check if name already exists.
if _, err := h.db.GetTemplate(ctx, req.Name); err == nil {
if !overwrite {
writeError(w, http.StatusConflict, "already_exists", "snapshot name already exists; use ?overwrite=true to replace")
return
}
// Delete existing template record and files.
h.db.DeleteTemplate(ctx, req.Name)
}
// Verify sandbox exists and is running or paused.
sb, err := h.db.GetSandbox(ctx, req.SandboxID)
if err != nil {
writeError(w, http.StatusNotFound, "not_found", "sandbox not found")
return
}
if sb.Status != "running" && sb.Status != "paused" {
writeError(w, http.StatusConflict, "invalid_state", "sandbox must be running or paused")
return
}
// Call host agent to create snapshot. If running, the agent pauses it first.
// The sandbox remains paused after this call.
resp, err := h.agent.CreateSnapshot(ctx, connect.NewRequest(&pb.CreateSnapshotRequest{
SandboxId: req.SandboxID,
Name: req.Name,
}))
if err != nil {
status, code, msg := agentErrToHTTP(err)
writeError(w, status, code, msg)
return
}
// Mark sandbox as paused (if it was running, it got paused by the snapshot).
if sb.Status != "paused" {
if _, err := h.db.UpdateSandboxStatus(ctx, db.UpdateSandboxStatusParams{
ID: req.SandboxID, Status: "paused",
}); err != nil {
slog.Error("failed to update sandbox status after snapshot", "sandbox_id", req.SandboxID, "error", err)
}
}
// Insert template record.
tmpl, err := h.db.InsertTemplate(ctx, db.InsertTemplateParams{
Name: req.Name,
Type: "snapshot",
Vcpus: pgtype.Int4{Int32: sb.Vcpus, Valid: true},
MemoryMb: pgtype.Int4{Int32: sb.MemoryMb, Valid: true},
SizeBytes: resp.Msg.SizeBytes,
})
if err != nil {
slog.Error("failed to insert template record", "name", req.Name, "error", err)
writeError(w, http.StatusInternalServerError, "db_error", "snapshot created but failed to record in database")
return
}
writeJSON(w, http.StatusCreated, templateToResponse(tmpl))
}
// List handles GET /v1/snapshots.
func (h *snapshotHandler) List(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
typeFilter := r.URL.Query().Get("type")
var templates []db.Template
var err error
if typeFilter != "" {
templates, err = h.db.ListTemplatesByType(ctx, typeFilter)
} else {
templates, err = h.db.ListTemplates(ctx)
}
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error", "failed to list templates")
return
}
resp := make([]snapshotResponse, len(templates))
for i, t := range templates {
resp[i] = templateToResponse(t)
}
writeJSON(w, http.StatusOK, resp)
}
// Delete handles DELETE /v1/snapshots/{name}.
func (h *snapshotHandler) Delete(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
ctx := r.Context()
if _, err := h.db.GetTemplate(ctx, name); err != nil {
writeError(w, http.StatusNotFound, "not_found", "template not found")
return
}
// Delete files on host agent.
if _, err := h.agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{
Name: name,
})); err != nil {
slog.Warn("delete snapshot: agent RPC failed", "name", name, "error", err)
}
if err := h.db.DeleteTemplate(ctx, name); err != nil {
writeError(w, http.StatusInternalServerError, "db_error", "failed to delete template record")
return
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -126,9 +126,13 @@ paths:
post:
summary: Pause a running sandbox
operationId: pauseSandbox
description: |
Takes a snapshot of the sandbox (VM state + memory + rootfs), then
destroys all running resources. The sandbox exists only as files on
disk and can be resumed later.
responses:
"200":
description: Sandbox paused
description: Sandbox paused (snapshot taken, resources released)
content:
application/json:
schema:
@ -151,9 +155,13 @@ paths:
post:
summary: Resume a paused sandbox
operationId: resumeSandbox
description: |
Restores a paused sandbox from its snapshot using UFFD for lazy
memory loading. Boots a fresh Firecracker process, sets up a new
network slot, and waits for envd to become ready.
responses:
"200":
description: Sandbox resumed
description: Sandbox resumed (new VM booted from snapshot)
content:
application/json:
schema:
@ -165,6 +173,85 @@ paths:
schema:
$ref: "#/components/schemas/Error"
/v1/snapshots:
post:
summary: Create a snapshot template
operationId: createSnapshot
description: |
Pauses a running sandbox, takes a full snapshot, copies the snapshot
files to the images directory as a reusable template, then destroys
the sandbox. The template can be used to create new sandboxes.
parameters:
- name: overwrite
in: query
required: false
schema:
type: string
enum: ["true"]
description: Set to "true" to overwrite an existing snapshot with the same name.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSnapshotRequest"
responses:
"201":
description: Snapshot created
content:
application/json:
schema:
$ref: "#/components/schemas/Template"
"409":
description: Name already exists or sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
summary: List templates
operationId: listSnapshots
parameters:
- name: type
in: query
required: false
schema:
type: string
enum: [base, snapshot]
description: Filter by template type.
responses:
"200":
description: List of templates
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Template"
/v1/snapshots/{name}:
parameters:
- name: name
in: path
required: true
schema:
type: string
delete:
summary: Delete a snapshot template
operationId: deleteSnapshot
description: Removes the snapshot files from disk and deletes the database record.
responses:
"204":
description: Snapshot deleted
"404":
description: Template not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/write:
parameters:
- name: id
@ -429,6 +516,38 @@ components:
type: string
format: date-time
CreateSnapshotRequest:
type: object
required: [sandbox_id]
properties:
sandbox_id:
type: string
description: ID of the running sandbox to snapshot.
name:
type: string
description: Name for the snapshot template. Auto-generated if omitted.
Template:
type: object
properties:
name:
type: string
type:
type: string
enum: [base, snapshot]
vcpus:
type: integer
nullable: true
memory_mb:
type: integer
nullable: true
size_bytes:
type: integer
format: int64
created_at:
type: string
format: date-time
ExecRequest:
type: object
required: [cmd]

View File

@ -62,10 +62,12 @@ func (rc *Reconciler) reconcile(ctx context.Context) {
alive[sb.SandboxId] = struct{}{}
}
// Get all DB sandboxes for this host that are running or paused.
// Get all DB sandboxes for this host that are running.
// Paused sandboxes are excluded: they are expected to not exist on the
// host agent because pause = snapshot + destroy resources.
dbSandboxes, err := rc.db.ListSandboxesByHostAndStatus(ctx, db.ListSandboxesByHostAndStatusParams{
HostID: rc.hostID,
Column2: []string{"running", "paused"},
Column2: []string{"running"},
})
if err != nil {
slog.Warn("reconciler: failed to list DB sandboxes", "error", err)

View File

@ -29,6 +29,7 @@ func New(queries *db.Queries, agent hostagentv1connect.HostAgentServiceClient) *
execStream := newExecStreamHandler(queries, agent)
files := newFilesHandler(queries, agent)
filesStream := newFilesStreamHandler(queries, agent)
snapshots := newSnapshotHandler(queries, agent)
// OpenAPI spec and docs.
r.Get("/openapi.yaml", serveOpenAPI)
@ -53,6 +54,13 @@ func New(queries *db.Queries, agent hostagentv1connect.HostAgentServiceClient) *
})
})
// Snapshot / template management.
r.Route("/v1/snapshots", func(r chi.Router) {
r.Post("/", snapshots.Create)
r.Get("/", snapshots.List)
r.Delete("/{name}", snapshots.Delete)
})
return &Server{router: r}
}

View File

@ -24,3 +24,12 @@ type Sandbox struct {
LastActiveAt pgtype.Timestamptz `json:"last_active_at"`
LastUpdated pgtype.Timestamptz `json:"last_updated"`
}
type Template struct {
Name string `json:"name"`
Type string `json:"type"`
Vcpus pgtype.Int4 `json:"vcpus"`
MemoryMb pgtype.Int4 `json:"memory_mb"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}

View File

@ -0,0 +1,135 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: templates.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const deleteTemplate = `-- name: DeleteTemplate :exec
DELETE FROM templates WHERE name = $1
`
func (q *Queries) DeleteTemplate(ctx context.Context, name string) error {
_, err := q.db.Exec(ctx, deleteTemplate, name)
return err
}
const getTemplate = `-- name: GetTemplate :one
SELECT name, type, vcpus, memory_mb, size_bytes, created_at FROM templates WHERE name = $1
`
func (q *Queries) GetTemplate(ctx context.Context, name string) (Template, error) {
row := q.db.QueryRow(ctx, getTemplate, name)
var i Template
err := row.Scan(
&i.Name,
&i.Type,
&i.Vcpus,
&i.MemoryMb,
&i.SizeBytes,
&i.CreatedAt,
)
return i, err
}
const insertTemplate = `-- name: InsertTemplate :one
INSERT INTO templates (name, type, vcpus, memory_mb, size_bytes)
VALUES ($1, $2, $3, $4, $5)
RETURNING name, type, vcpus, memory_mb, size_bytes, created_at
`
type InsertTemplateParams struct {
Name string `json:"name"`
Type string `json:"type"`
Vcpus pgtype.Int4 `json:"vcpus"`
MemoryMb pgtype.Int4 `json:"memory_mb"`
SizeBytes int64 `json:"size_bytes"`
}
func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
row := q.db.QueryRow(ctx, insertTemplate,
arg.Name,
arg.Type,
arg.Vcpus,
arg.MemoryMb,
arg.SizeBytes,
)
var i Template
err := row.Scan(
&i.Name,
&i.Type,
&i.Vcpus,
&i.MemoryMb,
&i.SizeBytes,
&i.CreatedAt,
)
return i, err
}
const listTemplates = `-- name: ListTemplates :many
SELECT name, type, vcpus, memory_mb, size_bytes, created_at FROM templates ORDER BY created_at DESC
`
func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
rows, err := q.db.Query(ctx, listTemplates)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Template
for rows.Next() {
var i Template
if err := rows.Scan(
&i.Name,
&i.Type,
&i.Vcpus,
&i.MemoryMb,
&i.SizeBytes,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTemplatesByType = `-- name: ListTemplatesByType :many
SELECT name, type, vcpus, memory_mb, size_bytes, created_at FROM templates WHERE type = $1 ORDER BY created_at DESC
`
func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Template, error) {
rows, err := q.db.Query(ctx, listTemplatesByType, type_)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Template
for rows.Next() {
var i Template
if err := rows.Scan(
&i.Name,
&i.Type,
&i.Vcpus,
&i.MemoryMb,
&i.SizeBytes,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -71,10 +71,39 @@ func (s *Server) ResumeSandbox(
ctx context.Context,
req *connect.Request[pb.ResumeSandboxRequest],
) (*connect.Response[pb.ResumeSandboxResponse], error) {
if err := s.mgr.Resume(ctx, req.Msg.SandboxId); err != nil {
sb, err := s.mgr.Resume(ctx, req.Msg.SandboxId)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return connect.NewResponse(&pb.ResumeSandboxResponse{}), nil
return connect.NewResponse(&pb.ResumeSandboxResponse{
SandboxId: sb.ID,
Status: string(sb.Status),
HostIp: sb.HostIP.String(),
}), nil
}
func (s *Server) CreateSnapshot(
ctx context.Context,
req *connect.Request[pb.CreateSnapshotRequest],
) (*connect.Response[pb.CreateSnapshotResponse], error) {
sizeBytes, err := s.mgr.CreateSnapshot(ctx, req.Msg.SandboxId, req.Msg.Name)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("create snapshot: %w", err))
}
return connect.NewResponse(&pb.CreateSnapshotResponse{
Name: req.Msg.Name,
SizeBytes: sizeBytes,
}), nil
}
func (s *Server) DeleteSnapshot(
ctx context.Context,
req *connect.Request[pb.DeleteSnapshotRequest],
) (*connect.Response[pb.DeleteSnapshotResponse], error) {
if err := s.mgr.DeleteSnapshot(req.Msg.Name); err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("delete snapshot: %w", err))
}
return connect.NewResponse(&pb.DeleteSnapshotResponse{}), nil
}
func (s *Server) Exec(
@ -352,15 +381,15 @@ func (s *Server) ListSandboxes(
infos := make([]*pb.SandboxInfo, len(sandboxes))
for i, sb := range sandboxes {
infos[i] = &pb.SandboxInfo{
SandboxId: sb.ID,
Status: string(sb.Status),
Template: sb.Template,
Vcpus: int32(sb.VCPUs),
MemoryMb: int32(sb.MemoryMB),
HostIp: sb.HostIP.String(),
CreatedAtUnix: sb.CreatedAt.Unix(),
SandboxId: sb.ID,
Status: string(sb.Status),
Template: sb.Template,
Vcpus: int32(sb.VCPUs),
MemoryMb: int32(sb.MemoryMB),
HostIp: sb.HostIP.String(),
CreatedAtUnix: sb.CreatedAt.Unix(),
LastActiveAtUnix: sb.LastActiveAt.Unix(),
TimeoutSec: int32(sb.TimeoutSec),
TimeoutSec: int32(sb.TimeoutSec),
}
}

View File

@ -14,3 +14,12 @@ func NewSandboxID() string {
}
return "sb-" + hex.EncodeToString(b)
}
// NewSnapshotName generates a snapshot name in the format "template-" + 8 hex chars.
func NewSnapshotName() string {
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
panic(fmt.Sprintf("crypto/rand failed: %v", err))
}
return "template-" + hex.EncodeToString(b)
}

View File

@ -9,19 +9,24 @@ import (
"sync"
"time"
"github.com/google/uuid"
"git.omukk.dev/wrenn/sandbox/internal/envdclient"
"git.omukk.dev/wrenn/sandbox/internal/filesystem"
"git.omukk.dev/wrenn/sandbox/internal/id"
"git.omukk.dev/wrenn/sandbox/internal/models"
"git.omukk.dev/wrenn/sandbox/internal/network"
"git.omukk.dev/wrenn/sandbox/internal/snapshot"
"git.omukk.dev/wrenn/sandbox/internal/uffd"
"git.omukk.dev/wrenn/sandbox/internal/vm"
)
// Config holds the paths and defaults for the sandbox manager.
type Config struct {
KernelPath string
ImagesDir string // directory containing base rootfs images (e.g., /var/lib/wrenn/images/minimal.ext4)
ImagesDir string // directory containing template images (e.g., /var/lib/wrenn/images/{name}/rootfs.ext4)
SandboxesDir string // directory for per-sandbox rootfs clones (e.g., /var/lib/wrenn/sandboxes)
SnapshotsDir string // directory for pause snapshots (e.g., /var/lib/wrenn/snapshots/{sandbox-id}/)
EnvdTimeout time.Duration
}
@ -38,8 +43,9 @@ type Manager struct {
// sandboxState holds the runtime state for a single sandbox.
type sandboxState struct {
models.Sandbox
slot *network.Slot
client *envdclient.Client
slot *network.Slot
client *envdclient.Client
uffdSocketPath string // non-empty for sandboxes restored from snapshot
}
// New creates a new sandbox manager.
@ -74,8 +80,13 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
template = "minimal"
}
// Resolve base rootfs image: /var/lib/wrenn/images/{template}.ext4
baseRootfs := filepath.Join(m.cfg.ImagesDir, template+".ext4")
// Check if template refers to a snapshot (has snapfile + memfile + header + rootfs).
if snapshot.IsSnapshot(m.cfg.ImagesDir, template) {
return m.createFromSnapshot(ctx, sandboxID, template, vcpus, memoryMB, timeoutSec)
}
// Resolve base rootfs image: /var/lib/wrenn/images/{template}/rootfs.ext4
baseRootfs := filepath.Join(m.cfg.ImagesDir, template, "rootfs.ext4")
if _, err := os.Stat(baseRootfs); err != nil {
return nil, fmt.Errorf("base rootfs not found at %s: %w", baseRootfs, err)
}
@ -168,18 +179,22 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
return &sb.Sandbox, nil
}
// Destroy stops and cleans up a sandbox.
// Destroy stops and cleans up a sandbox. If the sandbox is running, its VM,
// network, and rootfs are torn down. Any pause snapshot files are also removed.
func (m *Manager) Destroy(ctx context.Context, sandboxID string) error {
m.mu.Lock()
sb, ok := m.boxes[sandboxID]
if !ok {
m.mu.Unlock()
return fmt.Errorf("sandbox not found: %s", sandboxID)
if ok {
delete(m.boxes, sandboxID)
}
delete(m.boxes, sandboxID)
m.mu.Unlock()
m.cleanup(ctx, sb)
if ok {
m.cleanup(ctx, sb)
}
// Always clean up pause snapshot files (may exist if sandbox was paused).
snapshot.Remove(m.cfg.SnapshotsDir, sandboxID)
slog.Info("sandbox destroyed", "id", sandboxID)
return nil
@ -195,9 +210,14 @@ func (m *Manager) cleanup(ctx context.Context, sb *sandboxState) {
}
m.slots.Release(sb.SlotIndex)
os.Remove(sb.RootfsPath)
if sb.uffdSocketPath != "" {
os.Remove(sb.uffdSocketPath)
}
}
// Pause pauses a running sandbox.
// Pause takes a snapshot of a running sandbox, then destroys all resources.
// The sandbox's snapshot files are stored at SnapshotsDir/{sandboxID}/.
// After this call, the sandbox is no longer running but can be resumed.
func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
sb, err := m.get(sandboxID)
if err != nil {
@ -208,40 +228,386 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
return fmt.Errorf("sandbox %s is not running (status: %s)", sandboxID, sb.Status)
}
// Step 1: Pause the VM (freeze vCPUs).
if err := m.vm.Pause(ctx, sandboxID); err != nil {
return fmt.Errorf("pause VM: %w", err)
}
// Step 2: Take a full snapshot (snapfile + memfile).
if err := snapshot.EnsureDir(m.cfg.SnapshotsDir, sandboxID); err != nil {
return fmt.Errorf("create snapshot dir: %w", err)
}
snapDir := snapshot.DirPath(m.cfg.SnapshotsDir, sandboxID)
rawMemPath := filepath.Join(snapDir, "memfile.raw")
snapPath := snapshot.SnapPath(m.cfg.SnapshotsDir, sandboxID)
if err := m.vm.Snapshot(ctx, sandboxID, snapPath, rawMemPath); err != nil {
snapshot.Remove(m.cfg.SnapshotsDir, sandboxID)
return fmt.Errorf("create VM snapshot: %w", err)
}
// Step 3: Process the raw memfile into a compact diff + header.
buildID := uuid.New()
diffPath := snapshot.MemDiffPath(m.cfg.SnapshotsDir, sandboxID)
headerPath := snapshot.MemHeaderPath(m.cfg.SnapshotsDir, sandboxID)
if _, err := snapshot.ProcessMemfile(rawMemPath, diffPath, headerPath, buildID); err != nil {
snapshot.Remove(m.cfg.SnapshotsDir, sandboxID)
return fmt.Errorf("process memfile: %w", err)
}
// Remove the raw memfile — we only keep the compact diff.
os.Remove(rawMemPath)
// Step 4: Copy rootfs into snapshot dir.
snapshotRootfs := snapshot.RootfsPath(m.cfg.SnapshotsDir, sandboxID)
if err := filesystem.CloneRootfs(sb.RootfsPath, snapshotRootfs); err != nil {
snapshot.Remove(m.cfg.SnapshotsDir, sandboxID)
return fmt.Errorf("copy rootfs: %w", err)
}
// Step 5: Destroy the sandbox (free VM, network, rootfs clone).
m.mu.Lock()
sb.Status = models.StatusPaused
delete(m.boxes, sandboxID)
m.mu.Unlock()
slog.Info("sandbox paused", "id", sandboxID)
m.cleanup(ctx, sb)
slog.Info("sandbox paused (snapshot + destroy)", "id", sandboxID)
return nil
}
// Resume resumes a paused sandbox.
func (m *Manager) Resume(ctx context.Context, sandboxID string) error {
sb, err := m.get(sandboxID)
// Resume restores a paused sandbox from its snapshot using UFFD for
// lazy memory loading. The sandbox gets a new network slot.
func (m *Manager) Resume(ctx context.Context, sandboxID string) (*models.Sandbox, error) {
snapDir := m.cfg.SnapshotsDir
if !snapshot.Exists(snapDir, sandboxID) {
return nil, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
}
// Read the header to set up the UFFD memory source.
headerData, err := os.ReadFile(snapshot.MemHeaderPath(snapDir, sandboxID))
if err != nil {
return err
return nil, fmt.Errorf("read header: %w", err)
}
if sb.Status != models.StatusPaused {
return fmt.Errorf("sandbox %s is not paused (status: %s)", sandboxID, sb.Status)
header, err := snapshot.Deserialize(headerData)
if err != nil {
return nil, fmt.Errorf("deserialize header: %w", err)
}
if err := m.vm.Resume(ctx, sandboxID); err != nil {
return fmt.Errorf("resume VM: %w", err)
// Build diff file map (build ID → file path).
diffPaths := map[string]string{
header.Metadata.BuildID.String(): snapshot.MemDiffPath(snapDir, sandboxID),
}
source, err := uffd.NewDiffFileSource(header, diffPaths)
if err != nil {
return nil, fmt.Errorf("create memory source: %w", err)
}
// Clone snapshot rootfs for this sandbox.
snapshotRootfs := snapshot.RootfsPath(snapDir, sandboxID)
rootfsPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s-resume.ext4", sandboxID))
if err := filesystem.CloneRootfs(snapshotRootfs, rootfsPath); err != nil {
source.Close()
return nil, fmt.Errorf("clone snapshot rootfs: %w", err)
}
// Allocate network slot.
slotIdx, err := m.slots.Allocate()
if err != nil {
source.Close()
os.Remove(rootfsPath)
return nil, fmt.Errorf("allocate network slot: %w", err)
}
slot := network.NewSlot(slotIdx)
if err := network.CreateNetwork(slot); err != nil {
source.Close()
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("create network: %w", err)
}
// Start UFFD server.
uffdSocketPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s-uffd.sock", sandboxID))
os.Remove(uffdSocketPath) // Clean stale socket.
uffdServer := uffd.NewServer(uffdSocketPath, source)
if err := uffdServer.Start(ctx); err != nil {
source.Close()
network.RemoveNetwork(slot)
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("start uffd server: %w", err)
}
// Restore VM from snapshot.
vmCfg := vm.VMConfig{
SandboxID: sandboxID,
KernelPath: m.cfg.KernelPath,
RootfsPath: rootfsPath,
VCPUs: int(header.Metadata.Size / (1024 * 1024)), // Will be overridden by snapshot.
MemoryMB: int(header.Metadata.Size / (1024 * 1024)),
NetworkNamespace: slot.NamespaceID,
TapDevice: slot.TapName,
TapMAC: slot.TapMAC,
GuestIP: slot.GuestIP,
GatewayIP: slot.TapIP,
NetMask: slot.GuestNetMask,
}
snapPath := snapshot.SnapPath(snapDir, sandboxID)
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, snapPath, uffdSocketPath); err != nil {
uffdServer.Stop()
source.Close()
network.RemoveNetwork(slot)
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("restore VM from snapshot: %w", err)
}
// Wait for envd to be ready.
client := envdclient.New(slot.HostIP.String())
waitCtx, waitCancel := context.WithTimeout(ctx, m.cfg.EnvdTimeout)
defer waitCancel()
if err := client.WaitUntilReady(waitCtx); err != nil {
uffdServer.Stop()
source.Close()
m.vm.Destroy(context.Background(), sandboxID)
network.RemoveNetwork(slot)
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("wait for envd: %w", err)
}
now := time.Now()
sb := &sandboxState{
Sandbox: models.Sandbox{
ID: sandboxID,
Status: models.StatusRunning,
Template: "",
VCPUs: vmCfg.VCPUs,
MemoryMB: vmCfg.MemoryMB,
TimeoutSec: 0,
SlotIndex: slotIdx,
HostIP: slot.HostIP,
RootfsPath: rootfsPath,
CreatedAt: now,
LastActiveAt: now,
},
slot: slot,
client: client,
uffdSocketPath: uffdSocketPath,
}
m.mu.Lock()
sb.Status = models.StatusRunning
sb.LastActiveAt = time.Now()
m.boxes[sandboxID] = sb
m.mu.Unlock()
slog.Info("sandbox resumed", "id", sandboxID)
return nil
// Clean up the snapshot files now that the sandbox is running.
snapshot.Remove(snapDir, sandboxID)
slog.Info("sandbox resumed from snapshot",
"id", sandboxID,
"host_ip", slot.HostIP.String(),
)
return &sb.Sandbox, nil
}
// CreateSnapshot creates a reusable template from a sandbox. Works on both
// running and paused sandboxes. If the sandbox is running, it is paused first.
// The sandbox remains paused after this call (it can still be resumed).
// The template files are copied to ImagesDir/{name}/.
func (m *Manager) CreateSnapshot(ctx context.Context, sandboxID, name string) (int64, error) {
// If the sandbox is running, pause it first.
if _, err := m.get(sandboxID); err == nil {
if err := m.Pause(ctx, sandboxID); err != nil {
return 0, fmt.Errorf("pause sandbox: %w", err)
}
}
// At this point, pause snapshot files must exist in SnapshotsDir/{sandboxID}/.
if !snapshot.Exists(m.cfg.SnapshotsDir, sandboxID) {
return 0, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
}
// Copy snapshot files to ImagesDir/{name}/ as a reusable template.
if err := snapshot.EnsureDir(m.cfg.ImagesDir, name); err != nil {
return 0, fmt.Errorf("create template dir: %w", err)
}
srcDir := snapshot.DirPath(m.cfg.SnapshotsDir, sandboxID)
dstDir := snapshot.DirPath(m.cfg.ImagesDir, name)
for _, fname := range []string{snapshot.SnapFileName, snapshot.MemDiffName, snapshot.MemHeaderName, snapshot.RootfsFileName} {
src := filepath.Join(srcDir, fname)
dst := filepath.Join(dstDir, fname)
if err := filesystem.CloneRootfs(src, dst); err != nil {
snapshot.Remove(m.cfg.ImagesDir, name)
return 0, fmt.Errorf("copy %s: %w", fname, err)
}
}
sizeBytes, err := snapshot.DirSize(m.cfg.ImagesDir, name)
if err != nil {
slog.Warn("failed to calculate snapshot size", "error", err)
}
slog.Info("snapshot created",
"sandbox", sandboxID,
"name", name,
"size_bytes", sizeBytes,
)
return sizeBytes, nil
}
// DeleteSnapshot removes a snapshot template from disk.
func (m *Manager) DeleteSnapshot(name string) error {
return snapshot.Remove(m.cfg.ImagesDir, name)
}
// createFromSnapshot creates a new sandbox by restoring from a snapshot template
// in ImagesDir/{snapshotName}/. Uses UFFD for lazy memory loading.
func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotName string, vcpus, memoryMB, timeoutSec int) (*models.Sandbox, error) {
imagesDir := m.cfg.ImagesDir
// Read the header.
headerData, err := os.ReadFile(snapshot.MemHeaderPath(imagesDir, snapshotName))
if err != nil {
return nil, fmt.Errorf("read snapshot header: %w", err)
}
header, err := snapshot.Deserialize(headerData)
if err != nil {
return nil, fmt.Errorf("deserialize header: %w", err)
}
// Snapshot determines memory size. VCPUs are also baked into the
// snapshot state — the caller should pass the correct value from
// the template DB record.
memoryMB = int(header.Metadata.Size / (1024 * 1024))
// Build diff file map.
diffPaths := map[string]string{
header.Metadata.BuildID.String(): snapshot.MemDiffPath(imagesDir, snapshotName),
}
source, err := uffd.NewDiffFileSource(header, diffPaths)
if err != nil {
return nil, fmt.Errorf("create memory source: %w", err)
}
// Clone snapshot rootfs.
snapshotRootfs := snapshot.RootfsPath(imagesDir, snapshotName)
rootfsPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s-%s.ext4", sandboxID, snapshotName))
if err := filesystem.CloneRootfs(snapshotRootfs, rootfsPath); err != nil {
source.Close()
return nil, fmt.Errorf("clone snapshot rootfs: %w", err)
}
// Allocate network.
slotIdx, err := m.slots.Allocate()
if err != nil {
source.Close()
os.Remove(rootfsPath)
return nil, fmt.Errorf("allocate network slot: %w", err)
}
slot := network.NewSlot(slotIdx)
if err := network.CreateNetwork(slot); err != nil {
source.Close()
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("create network: %w", err)
}
// Start UFFD server.
uffdSocketPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s-uffd.sock", sandboxID))
os.Remove(uffdSocketPath)
uffdServer := uffd.NewServer(uffdSocketPath, source)
if err := uffdServer.Start(ctx); err != nil {
source.Close()
network.RemoveNetwork(slot)
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("start uffd server: %w", err)
}
// Restore VM.
vmCfg := vm.VMConfig{
SandboxID: sandboxID,
KernelPath: m.cfg.KernelPath,
RootfsPath: rootfsPath,
VCPUs: vcpus,
MemoryMB: memoryMB,
NetworkNamespace: slot.NamespaceID,
TapDevice: slot.TapName,
TapMAC: slot.TapMAC,
GuestIP: slot.GuestIP,
GatewayIP: slot.TapIP,
NetMask: slot.GuestNetMask,
}
snapPath := snapshot.SnapPath(imagesDir, snapshotName)
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, snapPath, uffdSocketPath); err != nil {
uffdServer.Stop()
source.Close()
network.RemoveNetwork(slot)
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("restore VM from snapshot: %w", err)
}
// Wait for envd.
client := envdclient.New(slot.HostIP.String())
waitCtx, waitCancel := context.WithTimeout(ctx, m.cfg.EnvdTimeout)
defer waitCancel()
if err := client.WaitUntilReady(waitCtx); err != nil {
uffdServer.Stop()
source.Close()
m.vm.Destroy(context.Background(), sandboxID)
network.RemoveNetwork(slot)
m.slots.Release(slotIdx)
os.Remove(rootfsPath)
return nil, fmt.Errorf("wait for envd: %w", err)
}
now := time.Now()
sb := &sandboxState{
Sandbox: models.Sandbox{
ID: sandboxID,
Status: models.StatusRunning,
Template: snapshotName,
VCPUs: vcpus,
MemoryMB: memoryMB,
TimeoutSec: timeoutSec,
SlotIndex: slotIdx,
HostIP: slot.HostIP,
RootfsPath: rootfsPath,
CreatedAt: now,
LastActiveAt: now,
},
slot: slot,
client: client,
uffdSocketPath: uffdSocketPath,
}
m.mu.Lock()
m.boxes[sandboxID] = sb
m.mu.Unlock()
slog.Info("sandbox created from snapshot",
"id", sandboxID,
"snapshot", snapshotName,
"host_ip", slot.HostIP.String(),
)
return &sb.Sandbox, nil
}
// Exec runs a command inside a sandbox.

220
internal/snapshot/header.go Normal file
View File

@ -0,0 +1,220 @@
// Package snapshot implements snapshot storage, header-based memory mapping,
// and memory file processing for Firecracker VM snapshots.
//
// The header system implements a generational copy-on-write memory mapping.
// Each snapshot generation stores only the blocks that changed since the
// previous generation. A Header contains a sorted list of BuildMap entries
// that together cover the entire memory address space, with each entry
// pointing to a specific generation's diff file.
//
// Inspired by e2b's snapshot system (Apache 2.0, modified by Omukk).
package snapshot
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/google/uuid"
)
const metadataVersion = 1
// Metadata is the fixed-size header prefix describing the snapshot memory layout.
// Binary layout (little-endian, 64 bytes total):
//
// Version uint64 (8 bytes)
// BlockSize uint64 (8 bytes)
// Size uint64 (8 bytes) — total memory size in bytes
// Generation uint64 (8 bytes)
// BuildID [16]byte (UUID)
// BaseBuildID [16]byte (UUID)
type Metadata struct {
Version uint64
BlockSize uint64
Size uint64
Generation uint64
BuildID uuid.UUID
BaseBuildID uuid.UUID
}
// NewMetadata creates metadata for a first-generation snapshot.
func NewMetadata(buildID uuid.UUID, blockSize, size uint64) *Metadata {
return &Metadata{
Version: metadataVersion,
Generation: 0,
BlockSize: blockSize,
Size: size,
BuildID: buildID,
BaseBuildID: buildID,
}
}
// NextGeneration creates metadata for the next generation in the chain.
func (m *Metadata) NextGeneration(buildID uuid.UUID) *Metadata {
return &Metadata{
Version: m.Version,
Generation: m.Generation + 1,
BlockSize: m.BlockSize,
Size: m.Size,
BuildID: buildID,
BaseBuildID: m.BaseBuildID,
}
}
// BuildMap maps a contiguous range of the memory address space to a specific
// generation's diff file. Binary layout (little-endian, 40 bytes):
//
// Offset uint64 — byte offset in the virtual address space
// Length uint64 — byte count (multiple of BlockSize)
// BuildID [16]byte — which generation's diff file, uuid.Nil = zero-fill
// BuildStorageOffset uint64 — byte offset within that generation's diff file
type BuildMap struct {
Offset uint64
Length uint64
BuildID uuid.UUID
BuildStorageOffset uint64
}
// Header is the in-memory representation of a snapshot's memory mapping.
// It provides O(log N) lookup from any memory offset to the correct
// generation's diff file and offset within it.
type Header struct {
Metadata *Metadata
Mapping []*BuildMap
// blockStarts tracks which block indices start a new BuildMap entry.
// startMap provides direct access from block index to the BuildMap.
blockStarts []bool
startMap map[int64]*BuildMap
}
// NewHeader creates a Header from metadata and mapping entries.
// If mapping is nil/empty, a single entry covering the full size is created.
func NewHeader(metadata *Metadata, mapping []*BuildMap) (*Header, error) {
if metadata.BlockSize == 0 {
return nil, fmt.Errorf("block size cannot be zero")
}
if len(mapping) == 0 {
mapping = []*BuildMap{{
Offset: 0,
Length: metadata.Size,
BuildID: metadata.BuildID,
BuildStorageOffset: 0,
}}
}
blocks := TotalBlocks(int64(metadata.Size), int64(metadata.BlockSize))
starts := make([]bool, blocks)
startMap := make(map[int64]*BuildMap, len(mapping))
for _, m := range mapping {
idx := BlockIdx(int64(m.Offset), int64(metadata.BlockSize))
if idx >= 0 && idx < blocks {
starts[idx] = true
startMap[idx] = m
}
}
return &Header{
Metadata: metadata,
Mapping: mapping,
blockStarts: starts,
startMap: startMap,
}, nil
}
// GetShiftedMapping resolves a memory offset to the corresponding diff file
// offset, remaining length, and build ID. This is the hot path called for
// every UFFD page fault.
func (h *Header) GetShiftedMapping(_ context.Context, offset int64) (mappedOffset int64, mappedLength int64, buildID *uuid.UUID, err error) {
if offset < 0 || offset >= int64(h.Metadata.Size) {
return 0, 0, nil, fmt.Errorf("offset %d out of bounds (size: %d)", offset, h.Metadata.Size)
}
blockSize := int64(h.Metadata.BlockSize)
block := BlockIdx(offset, blockSize)
// Walk backwards to find the BuildMap that contains this block.
start := block
for start >= 0 {
if h.blockStarts[start] {
break
}
start--
}
if start < 0 {
return 0, 0, nil, fmt.Errorf("no mapping found for offset %d", offset)
}
m, ok := h.startMap[start]
if !ok {
return 0, 0, nil, fmt.Errorf("no mapping at block %d", start)
}
shift := (block - start) * blockSize
if shift >= int64(m.Length) {
return 0, 0, nil, fmt.Errorf("offset %d beyond mapping end (mapping offset=%d, length=%d)", offset, m.Offset, m.Length)
}
return int64(m.BuildStorageOffset) + shift, int64(m.Length) - shift, &m.BuildID, nil
}
// Serialize writes metadata + mapping entries to binary (little-endian).
func Serialize(metadata *Metadata, mappings []*BuildMap) ([]byte, error) {
var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, metadata); err != nil {
return nil, fmt.Errorf("write metadata: %w", err)
}
for _, m := range mappings {
if err := binary.Write(&buf, binary.LittleEndian, m); err != nil {
return nil, fmt.Errorf("write mapping: %w", err)
}
}
return buf.Bytes(), nil
}
// Deserialize reads a header from binary data.
func Deserialize(data []byte) (*Header, error) {
reader := bytes.NewReader(data)
var metadata Metadata
if err := binary.Read(reader, binary.LittleEndian, &metadata); err != nil {
return nil, fmt.Errorf("read metadata: %w", err)
}
var mappings []*BuildMap
for {
var m BuildMap
if err := binary.Read(reader, binary.LittleEndian, &m); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("read mapping: %w", err)
}
mappings = append(mappings, &m)
}
return NewHeader(&metadata, mappings)
}
// Block index helpers.
func TotalBlocks(size, blockSize int64) int64 {
return (size + blockSize - 1) / blockSize
}
func BlockIdx(offset, blockSize int64) int64 {
return offset / blockSize
}
func BlockOffset(idx, blockSize int64) int64 {
return idx * blockSize
}

View File

@ -1 +1,101 @@
package snapshot
import (
"fmt"
"io/fs"
"os"
"path/filepath"
)
const (
SnapFileName = "snapfile"
MemDiffName = "memfile"
MemHeaderName = "memfile.header"
RootfsFileName = "rootfs.ext4"
)
// DirPath returns the snapshot directory for a given name.
func DirPath(baseDir, name string) string {
return filepath.Join(baseDir, name)
}
// SnapPath returns the path to the VM state snapshot file.
func SnapPath(baseDir, name string) string {
return filepath.Join(DirPath(baseDir, name), SnapFileName)
}
// MemDiffPath returns the path to the compact memory diff file.
func MemDiffPath(baseDir, name string) string {
return filepath.Join(DirPath(baseDir, name), MemDiffName)
}
// MemHeaderPath returns the path to the memory mapping header file.
func MemHeaderPath(baseDir, name string) string {
return filepath.Join(DirPath(baseDir, name), MemHeaderName)
}
// RootfsPath returns the path to the rootfs image.
func RootfsPath(baseDir, name string) string {
return filepath.Join(DirPath(baseDir, name), RootfsFileName)
}
// Exists reports whether a complete snapshot exists (all required files present).
func Exists(baseDir, name string) bool {
dir := DirPath(baseDir, name)
for _, f := range []string{SnapFileName, MemDiffName, MemHeaderName, RootfsFileName} {
if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
return false
}
}
return true
}
// IsTemplate reports whether a template image directory exists (has rootfs.ext4).
func IsTemplate(baseDir, name string) bool {
_, err := os.Stat(filepath.Join(DirPath(baseDir, name), RootfsFileName))
return err == nil
}
// IsSnapshot reports whether a directory is a snapshot (has all snapshot files).
func IsSnapshot(baseDir, name string) bool {
return Exists(baseDir, name)
}
// EnsureDir creates the snapshot directory if it doesn't exist.
func EnsureDir(baseDir, name string) error {
dir := DirPath(baseDir, name)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("create snapshot dir %s: %w", dir, err)
}
return nil
}
// Remove deletes the entire snapshot directory.
func Remove(baseDir, name string) error {
return os.RemoveAll(DirPath(baseDir, name))
}
// DirSize returns the total byte size of all files in the snapshot directory.
func DirSize(baseDir, name string) (int64, error) {
var total int64
dir := DirPath(baseDir, name)
err := filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
total += info.Size()
return nil
})
if err != nil {
return 0, fmt.Errorf("calculate snapshot size: %w", err)
}
return total, nil
}

View File

@ -0,0 +1,213 @@
package snapshot
import "github.com/google/uuid"
// CreateMapping converts a dirty-block bitset (represented as a []bool) into
// a sorted list of BuildMap entries. Consecutive dirty blocks are merged into
// a single entry. BuildStorageOffset tracks the sequential position in the
// compact diff file.
//
// Inspired by e2b's snapshot system (Apache 2.0, modified by Omukk).
func CreateMapping(buildID uuid.UUID, dirty []bool, blockSize int64) []*BuildMap {
var mappings []*BuildMap
var runStart int64 = -1
var runLength int64
var storageOffset uint64
for i, set := range dirty {
if !set {
if runLength > 0 {
mappings = append(mappings, &BuildMap{
Offset: uint64(runStart) * uint64(blockSize),
Length: uint64(runLength) * uint64(blockSize),
BuildID: buildID,
BuildStorageOffset: storageOffset,
})
storageOffset += uint64(runLength) * uint64(blockSize)
runLength = 0
}
runStart = -1
continue
}
if runStart < 0 {
runStart = int64(i)
runLength = 1
} else {
runLength++
}
}
if runLength > 0 {
mappings = append(mappings, &BuildMap{
Offset: uint64(runStart) * uint64(blockSize),
Length: uint64(runLength) * uint64(blockSize),
BuildID: buildID,
BuildStorageOffset: storageOffset,
})
}
return mappings
}
// MergeMappings overlays diffMapping on top of baseMapping. Where they overlap,
// diff takes priority. The result covers the entire address space.
//
// Both inputs must be sorted by Offset. The base mapping should cover the full size.
//
// Inspired by e2b's snapshot system (Apache 2.0, modified by Omukk).
func MergeMappings(baseMapping, diffMapping []*BuildMap) []*BuildMap {
if len(diffMapping) == 0 {
return baseMapping
}
// Work on a copy of baseMapping to avoid mutating the original.
baseCopy := make([]*BuildMap, len(baseMapping))
for i, m := range baseMapping {
cp := *m
baseCopy[i] = &cp
}
var result []*BuildMap
var bi, di int
for bi < len(baseCopy) && di < len(diffMapping) {
base := baseCopy[bi]
diff := diffMapping[di]
if base.Length == 0 {
bi++
continue
}
if diff.Length == 0 {
di++
continue
}
// No overlap: base entirely before diff.
if base.Offset+base.Length <= diff.Offset {
result = append(result, base)
bi++
continue
}
// No overlap: diff entirely before base.
if diff.Offset+diff.Length <= base.Offset {
result = append(result, diff)
di++
continue
}
// Base fully inside diff — skip base.
if base.Offset >= diff.Offset && base.Offset+base.Length <= diff.Offset+diff.Length {
bi++
continue
}
// Diff fully inside base — split base around diff.
if diff.Offset >= base.Offset && diff.Offset+diff.Length <= base.Offset+base.Length {
leftLen := int64(diff.Offset) - int64(base.Offset)
if leftLen > 0 {
result = append(result, &BuildMap{
Offset: base.Offset,
Length: uint64(leftLen),
BuildID: base.BuildID,
BuildStorageOffset: base.BuildStorageOffset,
})
}
result = append(result, diff)
di++
rightShift := int64(diff.Offset) + int64(diff.Length) - int64(base.Offset)
rightLen := int64(base.Length) - rightShift
if rightLen > 0 {
baseCopy[bi] = &BuildMap{
Offset: base.Offset + uint64(rightShift),
Length: uint64(rightLen),
BuildID: base.BuildID,
BuildStorageOffset: base.BuildStorageOffset + uint64(rightShift),
}
} else {
bi++
}
continue
}
// Base starts after diff with overlap — emit diff, trim base.
if base.Offset > diff.Offset {
result = append(result, diff)
di++
rightShift := int64(diff.Offset) + int64(diff.Length) - int64(base.Offset)
rightLen := int64(base.Length) - rightShift
if rightLen > 0 {
baseCopy[bi] = &BuildMap{
Offset: base.Offset + uint64(rightShift),
Length: uint64(rightLen),
BuildID: base.BuildID,
BuildStorageOffset: base.BuildStorageOffset + uint64(rightShift),
}
} else {
bi++
}
continue
}
// Diff starts after base with overlap — emit left part of base.
if diff.Offset > base.Offset {
leftLen := int64(diff.Offset) - int64(base.Offset)
if leftLen > 0 {
result = append(result, &BuildMap{
Offset: base.Offset,
Length: uint64(leftLen),
BuildID: base.BuildID,
BuildStorageOffset: base.BuildStorageOffset,
})
}
bi++
continue
}
}
// Append remaining entries.
result = append(result, baseCopy[bi:]...)
result = append(result, diffMapping[di:]...)
return result
}
// NormalizeMappings merges adjacent entries with the same BuildID.
func NormalizeMappings(mappings []*BuildMap) []*BuildMap {
if len(mappings) == 0 {
return nil
}
result := make([]*BuildMap, 0, len(mappings))
current := &BuildMap{
Offset: mappings[0].Offset,
Length: mappings[0].Length,
BuildID: mappings[0].BuildID,
BuildStorageOffset: mappings[0].BuildStorageOffset,
}
for i := 1; i < len(mappings); i++ {
m := mappings[i]
if m.BuildID == current.BuildID {
current.Length += m.Length
} else {
result = append(result, current)
current = &BuildMap{
Offset: m.Offset,
Length: m.Length,
BuildID: m.BuildID,
BuildStorageOffset: m.BuildStorageOffset,
}
}
}
result = append(result, current)
return result
}

View File

@ -0,0 +1,189 @@
package snapshot
import (
"fmt"
"io"
"os"
"github.com/google/uuid"
)
const (
// DefaultBlockSize is 4KB — standard page size for Firecracker.
DefaultBlockSize int64 = 4096
)
// ProcessMemfile reads a full memory file produced by Firecracker's
// PUT /snapshot/create, identifies non-zero blocks, and writes only those
// blocks to a compact diff file. Returns the Header describing the mapping.
//
// The output diff file contains non-zero blocks written sequentially.
// The header maps each block in the full address space to either:
// - A position in the diff file (for non-zero blocks)
// - uuid.Nil (for zero/empty blocks, served as zeros without I/O)
//
// buildID identifies this snapshot generation in the header chain.
func ProcessMemfile(memfilePath, diffPath, headerPath string, buildID uuid.UUID) (*Header, error) {
src, err := os.Open(memfilePath)
if err != nil {
return nil, fmt.Errorf("open memfile: %w", err)
}
defer src.Close()
info, err := src.Stat()
if err != nil {
return nil, fmt.Errorf("stat memfile: %w", err)
}
memSize := info.Size()
dst, err := os.Create(diffPath)
if err != nil {
return nil, fmt.Errorf("create diff file: %w", err)
}
defer dst.Close()
totalBlocks := TotalBlocks(memSize, DefaultBlockSize)
dirty := make([]bool, totalBlocks)
empty := make([]bool, totalBlocks)
buf := make([]byte, DefaultBlockSize)
for i := int64(0); i < totalBlocks; i++ {
n, err := io.ReadFull(src, buf)
if err != nil && err != io.ErrUnexpectedEOF {
return nil, fmt.Errorf("read block %d: %w", i, err)
}
// Zero-pad the last block if it's short.
if int64(n) < DefaultBlockSize {
for j := n; j < int(DefaultBlockSize); j++ {
buf[j] = 0
}
}
if isZeroBlock(buf) {
empty[i] = true
continue
}
dirty[i] = true
if _, err := dst.Write(buf); err != nil {
return nil, fmt.Errorf("write diff block %d: %w", i, err)
}
}
// Build header.
dirtyMappings := CreateMapping(buildID, dirty, DefaultBlockSize)
emptyMappings := CreateMapping(uuid.Nil, empty, DefaultBlockSize)
merged := MergeMappings(dirtyMappings, emptyMappings)
normalized := NormalizeMappings(merged)
metadata := NewMetadata(buildID, uint64(DefaultBlockSize), uint64(memSize))
header, err := NewHeader(metadata, normalized)
if err != nil {
return nil, fmt.Errorf("create header: %w", err)
}
// Write header to disk.
headerData, err := Serialize(metadata, normalized)
if err != nil {
return nil, fmt.Errorf("serialize header: %w", err)
}
if err := os.WriteFile(headerPath, headerData, 0644); err != nil {
return nil, fmt.Errorf("write header: %w", err)
}
return header, nil
}
// ProcessMemfileWithParent processes a memory file as a new generation on top
// of an existing parent header. The new diff file contains only blocks that
// differ from what the parent header maps. This is used for re-pause of a
// sandbox that was restored from a snapshot.
func ProcessMemfileWithParent(memfilePath, diffPath, headerPath string, parentHeader *Header, buildID uuid.UUID) (*Header, error) {
src, err := os.Open(memfilePath)
if err != nil {
return nil, fmt.Errorf("open memfile: %w", err)
}
defer src.Close()
info, err := src.Stat()
if err != nil {
return nil, fmt.Errorf("stat memfile: %w", err)
}
memSize := info.Size()
dst, err := os.Create(diffPath)
if err != nil {
return nil, fmt.Errorf("create diff file: %w", err)
}
defer dst.Close()
totalBlocks := TotalBlocks(memSize, DefaultBlockSize)
dirty := make([]bool, totalBlocks)
empty := make([]bool, totalBlocks)
buf := make([]byte, DefaultBlockSize)
for i := int64(0); i < totalBlocks; i++ {
n, err := io.ReadFull(src, buf)
if err != nil && err != io.ErrUnexpectedEOF {
return nil, fmt.Errorf("read block %d: %w", i, err)
}
if int64(n) < DefaultBlockSize {
for j := n; j < int(DefaultBlockSize); j++ {
buf[j] = 0
}
}
if isZeroBlock(buf) {
empty[i] = true
continue
}
dirty[i] = true
if _, err := dst.Write(buf); err != nil {
return nil, fmt.Errorf("write diff block %d: %w", i, err)
}
}
// Build new generation header merged with parent.
dirtyMappings := CreateMapping(buildID, dirty, DefaultBlockSize)
emptyMappings := CreateMapping(uuid.Nil, empty, DefaultBlockSize)
diffMapping := MergeMappings(dirtyMappings, emptyMappings)
merged := MergeMappings(parentHeader.Mapping, diffMapping)
normalized := NormalizeMappings(merged)
metadata := parentHeader.Metadata.NextGeneration(buildID)
header, err := NewHeader(metadata, normalized)
if err != nil {
return nil, fmt.Errorf("create header: %w", err)
}
headerData, err := Serialize(metadata, normalized)
if err != nil {
return nil, fmt.Errorf("serialize header: %w", err)
}
if err := os.WriteFile(headerPath, headerData, 0644); err != nil {
return nil, fmt.Errorf("write header: %w", err)
}
return header, nil
}
// isZeroBlock checks if a block is entirely zero bytes.
func isZeroBlock(block []byte) bool {
// Fast path: compare 8 bytes at a time.
for i := 0; i+8 <= len(block); i += 8 {
if block[i] != 0 || block[i+1] != 0 || block[i+2] != 0 || block[i+3] != 0 ||
block[i+4] != 0 || block[i+5] != 0 || block[i+6] != 0 || block[i+7] != 0 {
return false
}
}
// Tail bytes.
for i := len(block) &^ 7; i < len(block); i++ {
if block[i] != 0 {
return false
}
}
return true
}

87
internal/uffd/fd.go Normal file
View File

@ -0,0 +1,87 @@
// Package uffd implements a userfaultfd-based memory server for Firecracker
// snapshot restore. When a VM is restored from a snapshot, instead of loading
// the entire memory file upfront, the UFFD handler intercepts page faults
// and serves memory pages on demand from the snapshot's compact diff file.
//
// Inspired by e2b's UFFD implementation (Apache 2.0, modified by Omukk).
package uffd
/*
#include <sys/syscall.h>
#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
struct uffd_pagefault {
__u64 flags;
__u64 address;
__u32 ptid;
};
*/
import "C"
import (
"fmt"
"syscall"
"unsafe"
)
const (
UFFD_EVENT_PAGEFAULT = C.UFFD_EVENT_PAGEFAULT
UFFD_PAGEFAULT_FLAG_WRITE = C.UFFD_PAGEFAULT_FLAG_WRITE
UFFDIO_COPY = C.UFFDIO_COPY
UFFDIO_COPY_MODE_WP = C.UFFDIO_COPY_MODE_WP
)
type (
uffdMsg = C.struct_uffd_msg
uffdPagefault = C.struct_uffd_pagefault
uffdioCopy = C.struct_uffdio_copy
)
// fd wraps a userfaultfd file descriptor received from Firecracker.
type fd uintptr
// copy installs a page into guest memory at the given address using UFFDIO_COPY.
// mode controls write-protection: use UFFDIO_COPY_MODE_WP to preserve WP bit.
func (f fd) copy(addr, pagesize uintptr, data []byte, mode C.ulonglong) error {
alignedAddr := addr &^ (pagesize - 1)
cpy := uffdioCopy{
src: C.ulonglong(uintptr(unsafe.Pointer(&data[0]))),
dst: C.ulonglong(alignedAddr),
len: C.ulonglong(pagesize),
mode: mode,
copy: 0,
}
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(f), UFFDIO_COPY, uintptr(unsafe.Pointer(&cpy)))
if errno != 0 {
return errno
}
if cpy.copy != C.longlong(pagesize) {
return fmt.Errorf("UFFDIO_COPY copied %d bytes, expected %d", cpy.copy, pagesize)
}
return nil
}
// close closes the userfaultfd file descriptor.
func (f fd) close() error {
return syscall.Close(int(f))
}
// getMsgEvent extracts the event type from a uffd_msg.
func getMsgEvent(msg *uffdMsg) C.uchar {
return msg.event
}
// getMsgArg extracts the arg union from a uffd_msg.
func getMsgArg(msg *uffdMsg) [24]byte {
return msg.arg
}
// getPagefaultAddress extracts the faulting address from a uffd_pagefault.
func getPagefaultAddress(pf *uffdPagefault) uintptr {
return uintptr(pf.address)
}

35
internal/uffd/region.go Normal file
View File

@ -0,0 +1,35 @@
package uffd
import "fmt"
// Region is a mapping of guest memory to host virtual address space.
// Firecracker sends these as JSON when connecting to the UFFD socket.
// The JSON field names match Firecracker's UFFD protocol.
type Region struct {
BaseHostVirtAddr uintptr `json:"base_host_virt_addr"`
Size uintptr `json:"size"`
Offset uintptr `json:"offset"`
PageSize uintptr `json:"page_size_kib"` // Actually in bytes despite the name.
}
// Mapping translates between host virtual addresses and logical memory offsets.
type Mapping struct {
Regions []Region
}
// NewMapping creates a Mapping from a list of regions.
func NewMapping(regions []Region) *Mapping {
return &Mapping{Regions: regions}
}
// GetOffset converts a host virtual address to a logical memory file offset
// and returns the page size. This is called on every UFFD page fault.
func (m *Mapping) GetOffset(hostVirtAddr uintptr) (int64, uintptr, error) {
for _, r := range m.Regions {
if hostVirtAddr >= r.BaseHostVirtAddr && hostVirtAddr < r.BaseHostVirtAddr+r.Size {
offset := int64(hostVirtAddr-r.BaseHostVirtAddr) + int64(r.Offset)
return offset, r.PageSize, nil
}
}
return 0, 0, fmt.Errorf("address %#x not found in any memory region", hostVirtAddr)
}

352
internal/uffd/server.go Normal file
View File

@ -0,0 +1,352 @@
package uffd
/*
#include <linux/userfaultfd.h>
*/
import "C"
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net"
"os"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"git.omukk.dev/wrenn/sandbox/internal/snapshot"
)
const (
fdSize = 4
regionMappingsSize = 1024
maxConcurrentFaults = 4096
)
// MemorySource provides page data for the UFFD handler.
// Given a logical memory offset and a size, it returns the page data.
type MemorySource interface {
ReadPage(ctx context.Context, offset int64, size int64) ([]byte, error)
}
// Server manages the UFFD Unix socket lifecycle and page fault handling
// for a single Firecracker snapshot restore.
type Server struct {
socketPath string
source MemorySource
lis *net.UnixListener
readyCh chan struct{}
readyOnce sync.Once
doneCh chan struct{}
doneErr error
// exitPipe signals the poll loop to stop.
exitR *os.File
exitW *os.File
}
// NewServer creates a UFFD server that will listen on the given socket path
// and serve memory pages from the given source.
func NewServer(socketPath string, source MemorySource) *Server {
return &Server{
socketPath: socketPath,
source: source,
readyCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
}
// Start begins listening on the Unix socket. Firecracker will connect to this
// socket after loadSnapshot is called with the UFFD backend.
// Start returns immediately; the server runs in a background goroutine.
func (s *Server) Start(ctx context.Context) error {
lis, err := net.ListenUnix("unix", &net.UnixAddr{Name: s.socketPath, Net: "unix"})
if err != nil {
return fmt.Errorf("listen on uffd socket: %w", err)
}
s.lis = lis
if err := os.Chmod(s.socketPath, 0o777); err != nil {
lis.Close()
return fmt.Errorf("chmod uffd socket: %w", err)
}
// Create exit signal pipe.
r, w, err := os.Pipe()
if err != nil {
lis.Close()
return fmt.Errorf("create exit pipe: %w", err)
}
s.exitR = r
s.exitW = w
go func() {
defer close(s.doneCh)
s.doneErr = s.handle(ctx)
s.lis.Close()
s.exitR.Close()
s.exitW.Close()
s.readyOnce.Do(func() { close(s.readyCh) })
}()
return nil
}
// Ready returns a channel that is closed when the UFFD handler is ready
// (after Firecracker has connected and sent the uffd fd).
func (s *Server) Ready() <-chan struct{} {
return s.readyCh
}
// Stop signals the UFFD poll loop to exit and waits for it to finish.
func (s *Server) Stop() error {
// Write a byte to the exit pipe to wake the poll loop.
s.exitW.Write([]byte{0})
<-s.doneCh
return s.doneErr
}
// Wait blocks until the server exits.
func (s *Server) Wait() error {
<-s.doneCh
return s.doneErr
}
// handle accepts the Firecracker connection, receives the UFFD fd via
// SCM_RIGHTS, and runs the page fault poll loop.
func (s *Server) handle(ctx context.Context) error {
conn, err := s.lis.Accept()
if err != nil {
return fmt.Errorf("accept uffd connection: %w", err)
}
unixConn := conn.(*net.UnixConn)
defer unixConn.Close()
// Read the memory region mappings (JSON) and the UFFD fd (SCM_RIGHTS).
regionBuf := make([]byte, regionMappingsSize)
uffdBuf := make([]byte, syscall.CmsgSpace(fdSize))
nRegion, nFd, _, _, err := unixConn.ReadMsgUnix(regionBuf, uffdBuf)
if err != nil {
return fmt.Errorf("read uffd message: %w", err)
}
var regions []Region
if err := json.Unmarshal(regionBuf[:nRegion], &regions); err != nil {
return fmt.Errorf("parse memory regions: %w", err)
}
controlMsgs, err := syscall.ParseSocketControlMessage(uffdBuf[:nFd])
if err != nil {
return fmt.Errorf("parse control messages: %w", err)
}
if len(controlMsgs) != 1 {
return fmt.Errorf("expected 1 control message, got %d", len(controlMsgs))
}
fds, err := syscall.ParseUnixRights(&controlMsgs[0])
if err != nil {
return fmt.Errorf("parse unix rights: %w", err)
}
if len(fds) != 1 {
return fmt.Errorf("expected 1 fd, got %d", len(fds))
}
uffdFd := fd(fds[0])
defer uffdFd.close()
mapping := NewMapping(regions)
slog.Info("uffd handler connected",
"regions", len(regions),
"fd", int(uffdFd),
)
// Signal readiness.
s.readyOnce.Do(func() { close(s.readyCh) })
// Run the poll loop.
return s.serve(ctx, uffdFd, mapping)
}
// serve is the main poll loop. It polls the UFFD fd for page fault events
// and the exit pipe for shutdown signals.
func (s *Server) serve(ctx context.Context, uffdFd fd, mapping *Mapping) error {
pollFds := []unix.PollFd{
{Fd: int32(uffdFd), Events: unix.POLLIN},
{Fd: int32(s.exitR.Fd()), Events: unix.POLLIN},
}
var wg sync.WaitGroup
sem := make(chan struct{}, maxConcurrentFaults)
// Always wait for in-flight goroutines before returning, so the caller
// can safely close the uffd fd after serve returns.
defer wg.Wait()
for {
if _, err := unix.Poll(pollFds, -1); err != nil {
if err == unix.EINTR || err == unix.EAGAIN {
continue
}
return fmt.Errorf("poll: %w", err)
}
// Check exit signal.
if pollFds[1].Revents&unix.POLLIN != 0 {
return nil
}
if pollFds[0].Revents&unix.POLLIN == 0 {
continue
}
// Read the uffd_msg. The fd is O_NONBLOCK (set by Firecracker),
// so EAGAIN is expected — just go back to poll.
buf := make([]byte, unsafe.Sizeof(uffdMsg{}))
n, err := readUffdMsg(uffdFd, buf)
if err == syscall.EAGAIN {
continue
}
if err != nil {
return fmt.Errorf("read uffd msg: %w", err)
}
if n == 0 {
continue
}
msg := *(*uffdMsg)(unsafe.Pointer(&buf[0]))
if getMsgEvent(&msg) != UFFD_EVENT_PAGEFAULT {
return fmt.Errorf("unexpected uffd event type: %d", getMsgEvent(&msg))
}
arg := getMsgArg(&msg)
pf := *(*uffdPagefault)(unsafe.Pointer(&arg[0]))
addr := getPagefaultAddress(&pf)
offset, pagesize, err := mapping.GetOffset(addr)
if err != nil {
return fmt.Errorf("resolve address %#x: %w", addr, err)
}
sem <- struct{}{}
wg.Add(1)
go func() {
defer wg.Done()
defer func() { <-sem }()
if err := s.faultPage(ctx, uffdFd, addr, offset, pagesize); err != nil {
slog.Error("uffd fault page error",
"addr", fmt.Sprintf("%#x", addr),
"offset", offset,
"error", err,
)
}
}()
}
}
// readUffdMsg reads a single uffd_msg, retrying on EINTR.
// Returns (n, EAGAIN) if the non-blocking read has nothing available.
func readUffdMsg(uffdFd fd, buf []byte) (int, error) {
for {
n, err := syscall.Read(int(uffdFd), buf)
if err == syscall.EINTR {
continue
}
return n, err
}
}
// faultPage fetches a page from the memory source and copies it into
// guest memory via UFFDIO_COPY.
func (s *Server) faultPage(ctx context.Context, uffdFd fd, addr uintptr, offset int64, pagesize uintptr) error {
data, err := s.source.ReadPage(ctx, offset, int64(pagesize))
if err != nil {
return fmt.Errorf("read page at offset %d: %w", offset, err)
}
// Mode 0: no write-protect. Standard Firecracker does not register
// UFFD ranges with WP support, so UFFDIO_COPY_MODE_WP would fail.
if err := uffdFd.copy(addr, pagesize, data, 0); err != nil {
if errors.Is(err, unix.EEXIST) {
// Page already mapped (race with prefetch or concurrent fault).
return nil
}
return fmt.Errorf("uffdio_copy: %w", err)
}
return nil
}
// DiffFileSource serves pages from a snapshot's compact diff file using
// the header's block mapping to resolve offsets.
type DiffFileSource struct {
header *snapshot.Header
// diffs maps build ID → open file handle for each generation's diff file.
diffs map[string]*os.File
}
// NewDiffFileSource creates a memory source backed by snapshot diff files.
// diffs maps build ID string to the file path of each generation's diff file.
func NewDiffFileSource(header *snapshot.Header, diffPaths map[string]string) (*DiffFileSource, error) {
diffs := make(map[string]*os.File, len(diffPaths))
for id, path := range diffPaths {
f, err := os.Open(path)
if err != nil {
// Close already opened files.
for _, opened := range diffs {
opened.Close()
}
return nil, fmt.Errorf("open diff file %s: %w", path, err)
}
diffs[id] = f
}
return &DiffFileSource{header: header, diffs: diffs}, nil
}
// ReadPage resolves a memory offset through the header mapping and reads
// the corresponding page from the correct generation's diff file.
func (s *DiffFileSource) ReadPage(ctx context.Context, offset int64, size int64) ([]byte, error) {
mappedOffset, _, buildID, err := s.header.GetShiftedMapping(ctx, offset)
if err != nil {
return nil, fmt.Errorf("resolve offset %d: %w", offset, err)
}
// uuid.Nil means zero-fill (empty page).
var nilUUID [16]byte
if *buildID == nilUUID {
return make([]byte, size), nil
}
f, ok := s.diffs[buildID.String()]
if !ok {
return nil, fmt.Errorf("no diff file for build %s", buildID)
}
buf := make([]byte, size)
n, err := f.ReadAt(buf, mappedOffset)
if err != nil && int64(n) < size {
return nil, fmt.Errorf("read diff at offset %d: %w", mappedOffset, err)
}
return buf, nil
}
// Close closes all open diff file handles.
func (s *DiffFileSource) Close() error {
var errs []error
for _, f := range s.diffs {
if err := f.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}

View File

@ -70,7 +70,7 @@ func (c *VMConfig) applyDefaults() {
c.SocketPath = fmt.Sprintf("/tmp/fc-%s.sock", c.SandboxID)
}
if c.SandboxDir == "" {
c.SandboxDir = fmt.Sprintf("/tmp/fc-sandbox-%s", c.SandboxID)
c.SandboxDir = "/tmp/fc-vm"
}
if c.TapDevice == "" {
c.TapDevice = "tap0"

View File

@ -95,7 +95,7 @@ func (c *fcClient) setNetworkInterface(ctx context.Context, ifaceID, tapName, ma
// 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,
"vcpu_count": vcpus,
"mem_size_mib": memMB,
"smt": false,
})
@ -131,7 +131,7 @@ func (c *fcClient) createSnapshot(ctx context.Context, snapPath, memPath string)
})
}
// loadSnapshot loads a VM snapshot.
// loadSnapshot loads a VM snapshot from a file-backed memory image.
func (c *fcClient) loadSnapshot(ctx context.Context, snapPath, memPath string) error {
return c.do(ctx, http.MethodPut, "/snapshot/load", map[string]any{
"snapshot_path": snapPath,
@ -139,3 +139,17 @@ func (c *fcClient) loadSnapshot(ctx context.Context, snapPath, memPath string) e
"resume_vm": false,
})
}
// 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,
},
})
}

View File

@ -91,8 +91,10 @@ func configureVM(ctx context.Context, client *fcClient, cfg *VMConfig) error {
return fmt.Errorf("set boot source: %w", err)
}
// Root drive
if err := client.setRootfsDrive(ctx, "rootfs", cfg.RootfsPath, false); err != nil {
// Root drive — use the symlink path inside the mount namespace so that
// snapshots record a stable path that works on restore.
rootfsSymlink := cfg.SandboxDir + "/rootfs.ext4"
if err := client.setRootfsDrive(ctx, "rootfs", rootfsSymlink, false); err != nil {
return fmt.Errorf("set rootfs drive: %w", err)
}
@ -162,6 +164,92 @@ func (m *Manager) Destroy(ctx context.Context, sandboxID string) error {
return nil
}
// Snapshot creates a full VM snapshot. The VM must already be paused.
// Produces a snapfile (VM state) and a memfile (full memory dump).
func (m *Manager) Snapshot(ctx context.Context, sandboxID, snapPath, memPath string) error {
vm, ok := m.vms[sandboxID]
if !ok {
return fmt.Errorf("VM not found: %s", sandboxID)
}
if err := vm.client.createSnapshot(ctx, snapPath, memPath); err != nil {
return fmt.Errorf("create snapshot: %w", err)
}
slog.Info("VM snapshot created", "sandbox", sandboxID, "snap_path", snapPath)
return nil
}
// CreateFromSnapshot boots a new Firecracker VM by loading a snapshot
// using UFFD for lazy memory loading. The network namespace and TAP
// device must already be set up.
//
// No boot resources (kernel, drives, machine config) are configured —
// the snapshot carries all that state. The rootfs path recorded in the
// snapshot is resolved via a stable symlink at SandboxDir/rootfs.ext4
// inside the mount namespace (created by the start script in jailer.go).
//
// The sequence is:
// 1. Start FC process in mount+network namespace (creates tmpfs + rootfs symlink)
// 2. Wait for API socket
// 3. Load snapshot with UFFD backend
// 4. Resume VM execution
func (m *Manager) CreateFromSnapshot(ctx context.Context, cfg VMConfig, snapPath, uffdSocketPath string) (*VM, error) {
cfg.applyDefaults()
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
os.Remove(cfg.SocketPath)
slog.Info("restoring VM from snapshot",
"sandbox", cfg.SandboxID,
"snap_path", snapPath,
)
// Step 1: Launch the Firecracker process.
// The start script creates a tmpfs at SandboxDir and symlinks
// rootfs.ext4 → cfg.RootfsPath, so the snapshot's recorded rootfs
// path (/fc-vm/rootfs.ext4) resolves to the new clone.
proc, err := startProcess(ctx, &cfg)
if err != nil {
return nil, fmt.Errorf("start process: %w", err)
}
// Step 2: Wait for the API socket.
if err := waitForSocket(ctx, cfg.SocketPath, proc); err != nil {
proc.stop()
return nil, fmt.Errorf("wait for socket: %w", err)
}
client := newFCClient(cfg.SocketPath)
// Step 3: Load the snapshot with UFFD backend.
// No boot resources are configured — the snapshot carries kernel,
// drive, network, and machine config state.
if err := client.loadSnapshotWithUffd(ctx, snapPath, uffdSocketPath); err != nil {
proc.stop()
return nil, fmt.Errorf("load snapshot: %w", err)
}
// Step 4: Resume the VM.
if err := client.resumeVM(ctx); err != nil {
proc.stop()
return nil, fmt.Errorf("resume VM: %w", err)
}
vm := &VM{
Config: cfg,
process: proc,
client: client,
}
m.vms[cfg.SandboxID] = vm
slog.Info("VM restored from snapshot", "sandbox", cfg.SandboxID)
return vm, nil
}
// Get returns a running VM by sandbox ID.
func (m *Manager) Get(sandboxID string) (*VM, bool) {
vm, ok := m.vms[sandboxID]

View File

@ -369,6 +369,9 @@ func (x *ResumeSandboxRequest) GetSandboxId() string {
type ResumeSandboxResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
HostIp string `protobuf:"bytes,3,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -403,6 +406,211 @@ func (*ResumeSandboxResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{7}
}
func (x *ResumeSandboxResponse) GetSandboxId() string {
if x != nil {
return x.SandboxId
}
return ""
}
func (x *ResumeSandboxResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *ResumeSandboxResponse) GetHostIp() string {
if x != nil {
return x.HostIp
}
return ""
}
type CreateSnapshotRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateSnapshotRequest) Reset() {
*x = CreateSnapshotRequest{}
mi := &file_hostagent_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateSnapshotRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateSnapshotRequest) ProtoMessage() {}
func (x *CreateSnapshotRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateSnapshotRequest.ProtoReflect.Descriptor instead.
func (*CreateSnapshotRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{8}
}
func (x *CreateSnapshotRequest) GetSandboxId() string {
if x != nil {
return x.SandboxId
}
return ""
}
func (x *CreateSnapshotRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type CreateSnapshotResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
SizeBytes int64 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateSnapshotResponse) Reset() {
*x = CreateSnapshotResponse{}
mi := &file_hostagent_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateSnapshotResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateSnapshotResponse) ProtoMessage() {}
func (x *CreateSnapshotResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateSnapshotResponse.ProtoReflect.Descriptor instead.
func (*CreateSnapshotResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{9}
}
func (x *CreateSnapshotResponse) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *CreateSnapshotResponse) GetSizeBytes() int64 {
if x != nil {
return x.SizeBytes
}
return 0
}
type DeleteSnapshotRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteSnapshotRequest) Reset() {
*x = DeleteSnapshotRequest{}
mi := &file_hostagent_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteSnapshotRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteSnapshotRequest) ProtoMessage() {}
func (x *DeleteSnapshotRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteSnapshotRequest.ProtoReflect.Descriptor instead.
func (*DeleteSnapshotRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{10}
}
func (x *DeleteSnapshotRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type DeleteSnapshotResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteSnapshotResponse) Reset() {
*x = DeleteSnapshotResponse{}
mi := &file_hostagent_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteSnapshotResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteSnapshotResponse) ProtoMessage() {}
func (x *DeleteSnapshotResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteSnapshotResponse.ProtoReflect.Descriptor instead.
func (*DeleteSnapshotResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{11}
}
type ExecRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
@ -416,7 +624,7 @@ type ExecRequest struct {
func (x *ExecRequest) Reset() {
*x = ExecRequest{}
mi := &file_hostagent_proto_msgTypes[8]
mi := &file_hostagent_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -428,7 +636,7 @@ func (x *ExecRequest) String() string {
func (*ExecRequest) ProtoMessage() {}
func (x *ExecRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[8]
mi := &file_hostagent_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -441,7 +649,7 @@ func (x *ExecRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecRequest.ProtoReflect.Descriptor instead.
func (*ExecRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{8}
return file_hostagent_proto_rawDescGZIP(), []int{12}
}
func (x *ExecRequest) GetSandboxId() string {
@ -483,7 +691,7 @@ type ExecResponse struct {
func (x *ExecResponse) Reset() {
*x = ExecResponse{}
mi := &file_hostagent_proto_msgTypes[9]
mi := &file_hostagent_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -495,7 +703,7 @@ func (x *ExecResponse) String() string {
func (*ExecResponse) ProtoMessage() {}
func (x *ExecResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[9]
mi := &file_hostagent_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -508,7 +716,7 @@ func (x *ExecResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecResponse.ProtoReflect.Descriptor instead.
func (*ExecResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{9}
return file_hostagent_proto_rawDescGZIP(), []int{13}
}
func (x *ExecResponse) GetStdout() []byte {
@ -540,7 +748,7 @@ type ListSandboxesRequest struct {
func (x *ListSandboxesRequest) Reset() {
*x = ListSandboxesRequest{}
mi := &file_hostagent_proto_msgTypes[10]
mi := &file_hostagent_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -552,7 +760,7 @@ func (x *ListSandboxesRequest) String() string {
func (*ListSandboxesRequest) ProtoMessage() {}
func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[10]
mi := &file_hostagent_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -565,7 +773,7 @@ func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListSandboxesRequest.ProtoReflect.Descriptor instead.
func (*ListSandboxesRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{10}
return file_hostagent_proto_rawDescGZIP(), []int{14}
}
type ListSandboxesResponse struct {
@ -577,7 +785,7 @@ type ListSandboxesResponse struct {
func (x *ListSandboxesResponse) Reset() {
*x = ListSandboxesResponse{}
mi := &file_hostagent_proto_msgTypes[11]
mi := &file_hostagent_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -589,7 +797,7 @@ func (x *ListSandboxesResponse) String() string {
func (*ListSandboxesResponse) ProtoMessage() {}
func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[11]
mi := &file_hostagent_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -602,7 +810,7 @@ func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead.
func (*ListSandboxesResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{11}
return file_hostagent_proto_rawDescGZIP(), []int{15}
}
func (x *ListSandboxesResponse) GetSandboxes() []*SandboxInfo {
@ -629,7 +837,7 @@ type SandboxInfo struct {
func (x *SandboxInfo) Reset() {
*x = SandboxInfo{}
mi := &file_hostagent_proto_msgTypes[12]
mi := &file_hostagent_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -641,7 +849,7 @@ func (x *SandboxInfo) String() string {
func (*SandboxInfo) ProtoMessage() {}
func (x *SandboxInfo) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[12]
mi := &file_hostagent_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -654,7 +862,7 @@ func (x *SandboxInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use SandboxInfo.ProtoReflect.Descriptor instead.
func (*SandboxInfo) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{12}
return file_hostagent_proto_rawDescGZIP(), []int{16}
}
func (x *SandboxInfo) GetSandboxId() string {
@ -731,7 +939,7 @@ type WriteFileRequest struct {
func (x *WriteFileRequest) Reset() {
*x = WriteFileRequest{}
mi := &file_hostagent_proto_msgTypes[13]
mi := &file_hostagent_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -743,7 +951,7 @@ func (x *WriteFileRequest) String() string {
func (*WriteFileRequest) ProtoMessage() {}
func (x *WriteFileRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[13]
mi := &file_hostagent_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -756,7 +964,7 @@ func (x *WriteFileRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use WriteFileRequest.ProtoReflect.Descriptor instead.
func (*WriteFileRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{13}
return file_hostagent_proto_rawDescGZIP(), []int{17}
}
func (x *WriteFileRequest) GetSandboxId() string {
@ -788,7 +996,7 @@ type WriteFileResponse struct {
func (x *WriteFileResponse) Reset() {
*x = WriteFileResponse{}
mi := &file_hostagent_proto_msgTypes[14]
mi := &file_hostagent_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -800,7 +1008,7 @@ func (x *WriteFileResponse) String() string {
func (*WriteFileResponse) ProtoMessage() {}
func (x *WriteFileResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[14]
mi := &file_hostagent_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -813,7 +1021,7 @@ func (x *WriteFileResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use WriteFileResponse.ProtoReflect.Descriptor instead.
func (*WriteFileResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{14}
return file_hostagent_proto_rawDescGZIP(), []int{18}
}
type ReadFileRequest struct {
@ -826,7 +1034,7 @@ type ReadFileRequest struct {
func (x *ReadFileRequest) Reset() {
*x = ReadFileRequest{}
mi := &file_hostagent_proto_msgTypes[15]
mi := &file_hostagent_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -838,7 +1046,7 @@ func (x *ReadFileRequest) String() string {
func (*ReadFileRequest) ProtoMessage() {}
func (x *ReadFileRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[15]
mi := &file_hostagent_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -851,7 +1059,7 @@ func (x *ReadFileRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReadFileRequest.ProtoReflect.Descriptor instead.
func (*ReadFileRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{15}
return file_hostagent_proto_rawDescGZIP(), []int{19}
}
func (x *ReadFileRequest) GetSandboxId() string {
@ -877,7 +1085,7 @@ type ReadFileResponse struct {
func (x *ReadFileResponse) Reset() {
*x = ReadFileResponse{}
mi := &file_hostagent_proto_msgTypes[16]
mi := &file_hostagent_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -889,7 +1097,7 @@ func (x *ReadFileResponse) String() string {
func (*ReadFileResponse) ProtoMessage() {}
func (x *ReadFileResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[16]
mi := &file_hostagent_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -902,7 +1110,7 @@ func (x *ReadFileResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReadFileResponse.ProtoReflect.Descriptor instead.
func (*ReadFileResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{16}
return file_hostagent_proto_rawDescGZIP(), []int{20}
}
func (x *ReadFileResponse) GetContent() []byte {
@ -924,7 +1132,7 @@ type ExecStreamRequest struct {
func (x *ExecStreamRequest) Reset() {
*x = ExecStreamRequest{}
mi := &file_hostagent_proto_msgTypes[17]
mi := &file_hostagent_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -936,7 +1144,7 @@ func (x *ExecStreamRequest) String() string {
func (*ExecStreamRequest) ProtoMessage() {}
func (x *ExecStreamRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[17]
mi := &file_hostagent_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -949,7 +1157,7 @@ func (x *ExecStreamRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecStreamRequest.ProtoReflect.Descriptor instead.
func (*ExecStreamRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{17}
return file_hostagent_proto_rawDescGZIP(), []int{21}
}
func (x *ExecStreamRequest) GetSandboxId() string {
@ -994,7 +1202,7 @@ type ExecStreamResponse struct {
func (x *ExecStreamResponse) Reset() {
*x = ExecStreamResponse{}
mi := &file_hostagent_proto_msgTypes[18]
mi := &file_hostagent_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1006,7 +1214,7 @@ func (x *ExecStreamResponse) String() string {
func (*ExecStreamResponse) ProtoMessage() {}
func (x *ExecStreamResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[18]
mi := &file_hostagent_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1019,7 +1227,7 @@ func (x *ExecStreamResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecStreamResponse.ProtoReflect.Descriptor instead.
func (*ExecStreamResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{18}
return file_hostagent_proto_rawDescGZIP(), []int{22}
}
func (x *ExecStreamResponse) GetEvent() isExecStreamResponse_Event {
@ -1087,7 +1295,7 @@ type ExecStreamStart struct {
func (x *ExecStreamStart) Reset() {
*x = ExecStreamStart{}
mi := &file_hostagent_proto_msgTypes[19]
mi := &file_hostagent_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1099,7 +1307,7 @@ func (x *ExecStreamStart) String() string {
func (*ExecStreamStart) ProtoMessage() {}
func (x *ExecStreamStart) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[19]
mi := &file_hostagent_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1112,7 +1320,7 @@ func (x *ExecStreamStart) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecStreamStart.ProtoReflect.Descriptor instead.
func (*ExecStreamStart) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{19}
return file_hostagent_proto_rawDescGZIP(), []int{23}
}
func (x *ExecStreamStart) GetPid() uint32 {
@ -1135,7 +1343,7 @@ type ExecStreamData struct {
func (x *ExecStreamData) Reset() {
*x = ExecStreamData{}
mi := &file_hostagent_proto_msgTypes[20]
mi := &file_hostagent_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1147,7 +1355,7 @@ func (x *ExecStreamData) String() string {
func (*ExecStreamData) ProtoMessage() {}
func (x *ExecStreamData) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[20]
mi := &file_hostagent_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1160,7 +1368,7 @@ func (x *ExecStreamData) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecStreamData.ProtoReflect.Descriptor instead.
func (*ExecStreamData) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{20}
return file_hostagent_proto_rawDescGZIP(), []int{24}
}
func (x *ExecStreamData) GetOutput() isExecStreamData_Output {
@ -1214,7 +1422,7 @@ type ExecStreamEnd struct {
func (x *ExecStreamEnd) Reset() {
*x = ExecStreamEnd{}
mi := &file_hostagent_proto_msgTypes[21]
mi := &file_hostagent_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1226,7 +1434,7 @@ func (x *ExecStreamEnd) String() string {
func (*ExecStreamEnd) ProtoMessage() {}
func (x *ExecStreamEnd) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[21]
mi := &file_hostagent_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1239,7 +1447,7 @@ func (x *ExecStreamEnd) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecStreamEnd.ProtoReflect.Descriptor instead.
func (*ExecStreamEnd) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{21}
return file_hostagent_proto_rawDescGZIP(), []int{25}
}
func (x *ExecStreamEnd) GetExitCode() int32 {
@ -1269,7 +1477,7 @@ type WriteFileStreamRequest struct {
func (x *WriteFileStreamRequest) Reset() {
*x = WriteFileStreamRequest{}
mi := &file_hostagent_proto_msgTypes[22]
mi := &file_hostagent_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1281,7 +1489,7 @@ func (x *WriteFileStreamRequest) String() string {
func (*WriteFileStreamRequest) ProtoMessage() {}
func (x *WriteFileStreamRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[22]
mi := &file_hostagent_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1294,7 +1502,7 @@ func (x *WriteFileStreamRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use WriteFileStreamRequest.ProtoReflect.Descriptor instead.
func (*WriteFileStreamRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{22}
return file_hostagent_proto_rawDescGZIP(), []int{26}
}
func (x *WriteFileStreamRequest) GetContent() isWriteFileStreamRequest_Content {
@ -1348,7 +1556,7 @@ type WriteFileStreamMeta struct {
func (x *WriteFileStreamMeta) Reset() {
*x = WriteFileStreamMeta{}
mi := &file_hostagent_proto_msgTypes[23]
mi := &file_hostagent_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1360,7 +1568,7 @@ func (x *WriteFileStreamMeta) String() string {
func (*WriteFileStreamMeta) ProtoMessage() {}
func (x *WriteFileStreamMeta) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[23]
mi := &file_hostagent_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1373,7 +1581,7 @@ func (x *WriteFileStreamMeta) ProtoReflect() protoreflect.Message {
// Deprecated: Use WriteFileStreamMeta.ProtoReflect.Descriptor instead.
func (*WriteFileStreamMeta) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{23}
return file_hostagent_proto_rawDescGZIP(), []int{27}
}
func (x *WriteFileStreamMeta) GetSandboxId() string {
@ -1398,7 +1606,7 @@ type WriteFileStreamResponse struct {
func (x *WriteFileStreamResponse) Reset() {
*x = WriteFileStreamResponse{}
mi := &file_hostagent_proto_msgTypes[24]
mi := &file_hostagent_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1410,7 +1618,7 @@ func (x *WriteFileStreamResponse) String() string {
func (*WriteFileStreamResponse) ProtoMessage() {}
func (x *WriteFileStreamResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[24]
mi := &file_hostagent_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1423,7 +1631,7 @@ func (x *WriteFileStreamResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use WriteFileStreamResponse.ProtoReflect.Descriptor instead.
func (*WriteFileStreamResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{24}
return file_hostagent_proto_rawDescGZIP(), []int{28}
}
type ReadFileStreamRequest struct {
@ -1436,7 +1644,7 @@ type ReadFileStreamRequest struct {
func (x *ReadFileStreamRequest) Reset() {
*x = ReadFileStreamRequest{}
mi := &file_hostagent_proto_msgTypes[25]
mi := &file_hostagent_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1448,7 +1656,7 @@ func (x *ReadFileStreamRequest) String() string {
func (*ReadFileStreamRequest) ProtoMessage() {}
func (x *ReadFileStreamRequest) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[25]
mi := &file_hostagent_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1461,7 +1669,7 @@ func (x *ReadFileStreamRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReadFileStreamRequest.ProtoReflect.Descriptor instead.
func (*ReadFileStreamRequest) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{25}
return file_hostagent_proto_rawDescGZIP(), []int{29}
}
func (x *ReadFileStreamRequest) GetSandboxId() string {
@ -1487,7 +1695,7 @@ type ReadFileStreamResponse struct {
func (x *ReadFileStreamResponse) Reset() {
*x = ReadFileStreamResponse{}
mi := &file_hostagent_proto_msgTypes[26]
mi := &file_hostagent_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1499,7 +1707,7 @@ func (x *ReadFileStreamResponse) String() string {
func (*ReadFileStreamResponse) ProtoMessage() {}
func (x *ReadFileStreamResponse) ProtoReflect() protoreflect.Message {
mi := &file_hostagent_proto_msgTypes[26]
mi := &file_hostagent_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1512,7 +1720,7 @@ func (x *ReadFileStreamResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReadFileStreamResponse.ProtoReflect.Descriptor instead.
func (*ReadFileStreamResponse) Descriptor() ([]byte, []int) {
return file_hostagent_proto_rawDescGZIP(), []int{26}
return file_hostagent_proto_rawDescGZIP(), []int{30}
}
func (x *ReadFileStreamResponse) GetChunk() []byte {
@ -1550,8 +1758,23 @@ const file_hostagent_proto_rawDesc = "" +
"\x14PauseSandboxResponse\"5\n" +
"\x14ResumeSandboxRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x17\n" +
"\x15ResumeSandboxResponse\"s\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\"g\n" +
"\x15ResumeSandboxResponse\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x16\n" +
"\x06status\x18\x02 \x01(\tR\x06status\x12\x17\n" +
"\ahost_ip\x18\x03 \x01(\tR\x06hostIp\"J\n" +
"\x15CreateSnapshotRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\"K\n" +
"\x16CreateSnapshotResponse\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
"\n" +
"size_bytes\x18\x02 \x01(\x03R\tsizeBytes\"+\n" +
"\x15DeleteSnapshotRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"\x18\n" +
"\x16DeleteSnapshotResponse\"s\n" +
"\vExecRequest\x12\x1d\n" +
"\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x10\n" +
@ -1625,7 +1848,7 @@ const file_hostagent_proto_rawDesc = "" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\".\n" +
"\x16ReadFileStreamResponse\x12\x14\n" +
"\x05chunk\x18\x01 \x01(\fR\x05chunk2\xc0\a\n" +
"\x05chunk\x18\x01 \x01(\fR\x05chunk2\xfa\b\n" +
"\x10HostAgentService\x12X\n" +
"\rCreateSandbox\x12\".hostagent.v1.CreateSandboxRequest\x1a#.hostagent.v1.CreateSandboxResponse\x12[\n" +
"\x0eDestroySandbox\x12#.hostagent.v1.DestroySandboxRequest\x1a$.hostagent.v1.DestroySandboxResponse\x12U\n" +
@ -1634,7 +1857,9 @@ const file_hostagent_proto_rawDesc = "" +
"\x04Exec\x12\x19.hostagent.v1.ExecRequest\x1a\x1a.hostagent.v1.ExecResponse\x12X\n" +
"\rListSandboxes\x12\".hostagent.v1.ListSandboxesRequest\x1a#.hostagent.v1.ListSandboxesResponse\x12L\n" +
"\tWriteFile\x12\x1e.hostagent.v1.WriteFileRequest\x1a\x1f.hostagent.v1.WriteFileResponse\x12I\n" +
"\bReadFile\x12\x1d.hostagent.v1.ReadFileRequest\x1a\x1e.hostagent.v1.ReadFileResponse\x12Q\n" +
"\bReadFile\x12\x1d.hostagent.v1.ReadFileRequest\x1a\x1e.hostagent.v1.ReadFileResponse\x12[\n" +
"\x0eCreateSnapshot\x12#.hostagent.v1.CreateSnapshotRequest\x1a$.hostagent.v1.CreateSnapshotResponse\x12[\n" +
"\x0eDeleteSnapshot\x12#.hostagent.v1.DeleteSnapshotRequest\x1a$.hostagent.v1.DeleteSnapshotResponse\x12Q\n" +
"\n" +
"ExecStream\x12\x1f.hostagent.v1.ExecStreamRequest\x1a .hostagent.v1.ExecStreamResponse0\x01\x12`\n" +
"\x0fWriteFileStream\x12$.hostagent.v1.WriteFileStreamRequest\x1a%.hostagent.v1.WriteFileStreamResponse(\x01\x12]\n" +
@ -1653,7 +1878,7 @@ func file_hostagent_proto_rawDescGZIP() []byte {
return file_hostagent_proto_rawDescData
}
var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 31)
var file_hostagent_proto_goTypes = []any{
(*CreateSandboxRequest)(nil), // 0: hostagent.v1.CreateSandboxRequest
(*CreateSandboxResponse)(nil), // 1: hostagent.v1.CreateSandboxResponse
@ -1663,56 +1888,64 @@ var file_hostagent_proto_goTypes = []any{
(*PauseSandboxResponse)(nil), // 5: hostagent.v1.PauseSandboxResponse
(*ResumeSandboxRequest)(nil), // 6: hostagent.v1.ResumeSandboxRequest
(*ResumeSandboxResponse)(nil), // 7: hostagent.v1.ResumeSandboxResponse
(*ExecRequest)(nil), // 8: hostagent.v1.ExecRequest
(*ExecResponse)(nil), // 9: hostagent.v1.ExecResponse
(*ListSandboxesRequest)(nil), // 10: hostagent.v1.ListSandboxesRequest
(*ListSandboxesResponse)(nil), // 11: hostagent.v1.ListSandboxesResponse
(*SandboxInfo)(nil), // 12: hostagent.v1.SandboxInfo
(*WriteFileRequest)(nil), // 13: hostagent.v1.WriteFileRequest
(*WriteFileResponse)(nil), // 14: hostagent.v1.WriteFileResponse
(*ReadFileRequest)(nil), // 15: hostagent.v1.ReadFileRequest
(*ReadFileResponse)(nil), // 16: hostagent.v1.ReadFileResponse
(*ExecStreamRequest)(nil), // 17: hostagent.v1.ExecStreamRequest
(*ExecStreamResponse)(nil), // 18: hostagent.v1.ExecStreamResponse
(*ExecStreamStart)(nil), // 19: hostagent.v1.ExecStreamStart
(*ExecStreamData)(nil), // 20: hostagent.v1.ExecStreamData
(*ExecStreamEnd)(nil), // 21: hostagent.v1.ExecStreamEnd
(*WriteFileStreamRequest)(nil), // 22: hostagent.v1.WriteFileStreamRequest
(*WriteFileStreamMeta)(nil), // 23: hostagent.v1.WriteFileStreamMeta
(*WriteFileStreamResponse)(nil), // 24: hostagent.v1.WriteFileStreamResponse
(*ReadFileStreamRequest)(nil), // 25: hostagent.v1.ReadFileStreamRequest
(*ReadFileStreamResponse)(nil), // 26: hostagent.v1.ReadFileStreamResponse
(*CreateSnapshotRequest)(nil), // 8: hostagent.v1.CreateSnapshotRequest
(*CreateSnapshotResponse)(nil), // 9: hostagent.v1.CreateSnapshotResponse
(*DeleteSnapshotRequest)(nil), // 10: hostagent.v1.DeleteSnapshotRequest
(*DeleteSnapshotResponse)(nil), // 11: hostagent.v1.DeleteSnapshotResponse
(*ExecRequest)(nil), // 12: hostagent.v1.ExecRequest
(*ExecResponse)(nil), // 13: hostagent.v1.ExecResponse
(*ListSandboxesRequest)(nil), // 14: hostagent.v1.ListSandboxesRequest
(*ListSandboxesResponse)(nil), // 15: hostagent.v1.ListSandboxesResponse
(*SandboxInfo)(nil), // 16: hostagent.v1.SandboxInfo
(*WriteFileRequest)(nil), // 17: hostagent.v1.WriteFileRequest
(*WriteFileResponse)(nil), // 18: hostagent.v1.WriteFileResponse
(*ReadFileRequest)(nil), // 19: hostagent.v1.ReadFileRequest
(*ReadFileResponse)(nil), // 20: hostagent.v1.ReadFileResponse
(*ExecStreamRequest)(nil), // 21: hostagent.v1.ExecStreamRequest
(*ExecStreamResponse)(nil), // 22: hostagent.v1.ExecStreamResponse
(*ExecStreamStart)(nil), // 23: hostagent.v1.ExecStreamStart
(*ExecStreamData)(nil), // 24: hostagent.v1.ExecStreamData
(*ExecStreamEnd)(nil), // 25: hostagent.v1.ExecStreamEnd
(*WriteFileStreamRequest)(nil), // 26: hostagent.v1.WriteFileStreamRequest
(*WriteFileStreamMeta)(nil), // 27: hostagent.v1.WriteFileStreamMeta
(*WriteFileStreamResponse)(nil), // 28: hostagent.v1.WriteFileStreamResponse
(*ReadFileStreamRequest)(nil), // 29: hostagent.v1.ReadFileStreamRequest
(*ReadFileStreamResponse)(nil), // 30: hostagent.v1.ReadFileStreamResponse
}
var file_hostagent_proto_depIdxs = []int32{
12, // 0: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo
19, // 1: hostagent.v1.ExecStreamResponse.start:type_name -> hostagent.v1.ExecStreamStart
20, // 2: hostagent.v1.ExecStreamResponse.data:type_name -> hostagent.v1.ExecStreamData
21, // 3: hostagent.v1.ExecStreamResponse.end:type_name -> hostagent.v1.ExecStreamEnd
23, // 4: hostagent.v1.WriteFileStreamRequest.meta:type_name -> hostagent.v1.WriteFileStreamMeta
16, // 0: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo
23, // 1: hostagent.v1.ExecStreamResponse.start:type_name -> hostagent.v1.ExecStreamStart
24, // 2: hostagent.v1.ExecStreamResponse.data:type_name -> hostagent.v1.ExecStreamData
25, // 3: hostagent.v1.ExecStreamResponse.end:type_name -> hostagent.v1.ExecStreamEnd
27, // 4: hostagent.v1.WriteFileStreamRequest.meta:type_name -> hostagent.v1.WriteFileStreamMeta
0, // 5: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest
2, // 6: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest
4, // 7: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest
6, // 8: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest
8, // 9: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest
10, // 10: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest
13, // 11: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest
15, // 12: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest
17, // 13: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest
22, // 14: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest
25, // 15: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest
1, // 16: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse
3, // 17: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse
5, // 18: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse
7, // 19: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse
9, // 20: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse
11, // 21: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse
14, // 22: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse
16, // 23: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse
18, // 24: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse
24, // 25: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse
26, // 26: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse
16, // [16:27] is the sub-list for method output_type
5, // [5:16] is the sub-list for method input_type
12, // 9: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest
14, // 10: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest
17, // 11: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest
19, // 12: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest
8, // 13: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest
10, // 14: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest
21, // 15: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest
26, // 16: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest
29, // 17: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest
1, // 18: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse
3, // 19: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse
5, // 20: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse
7, // 21: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse
13, // 22: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse
15, // 23: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse
18, // 24: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse
20, // 25: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse
9, // 26: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse
11, // 27: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse
22, // 28: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse
28, // 29: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse
30, // 30: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse
18, // [18:31] is the sub-list for method output_type
5, // [5:18] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
@ -1723,16 +1956,16 @@ func file_hostagent_proto_init() {
if File_hostagent_proto != nil {
return
}
file_hostagent_proto_msgTypes[18].OneofWrappers = []any{
file_hostagent_proto_msgTypes[22].OneofWrappers = []any{
(*ExecStreamResponse_Start)(nil),
(*ExecStreamResponse_Data)(nil),
(*ExecStreamResponse_End)(nil),
}
file_hostagent_proto_msgTypes[20].OneofWrappers = []any{
file_hostagent_proto_msgTypes[24].OneofWrappers = []any{
(*ExecStreamData_Stdout)(nil),
(*ExecStreamData_Stderr)(nil),
}
file_hostagent_proto_msgTypes[22].OneofWrappers = []any{
file_hostagent_proto_msgTypes[26].OneofWrappers = []any{
(*WriteFileStreamRequest_Meta)(nil),
(*WriteFileStreamRequest_Chunk)(nil),
}
@ -1742,7 +1975,7 @@ func file_hostagent_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)),
NumEnums: 0,
NumMessages: 27,
NumMessages: 31,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -56,6 +56,12 @@ const (
// HostAgentServiceReadFileProcedure is the fully-qualified name of the HostAgentService's ReadFile
// RPC.
HostAgentServiceReadFileProcedure = "/hostagent.v1.HostAgentService/ReadFile"
// HostAgentServiceCreateSnapshotProcedure is the fully-qualified name of the HostAgentService's
// CreateSnapshot RPC.
HostAgentServiceCreateSnapshotProcedure = "/hostagent.v1.HostAgentService/CreateSnapshot"
// HostAgentServiceDeleteSnapshotProcedure is the fully-qualified name of the HostAgentService's
// DeleteSnapshot RPC.
HostAgentServiceDeleteSnapshotProcedure = "/hostagent.v1.HostAgentService/DeleteSnapshot"
// HostAgentServiceExecStreamProcedure is the fully-qualified name of the HostAgentService's
// ExecStream RPC.
HostAgentServiceExecStreamProcedure = "/hostagent.v1.HostAgentService/ExecStream"
@ -85,6 +91,11 @@ type HostAgentServiceClient interface {
WriteFile(context.Context, *connect.Request[gen.WriteFileRequest]) (*connect.Response[gen.WriteFileResponse], error)
// ReadFile reads a file from inside a sandbox.
ReadFile(context.Context, *connect.Request[gen.ReadFileRequest]) (*connect.Response[gen.ReadFileResponse], error)
// CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable
// template, and destroys the sandbox.
CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error)
// DeleteSnapshot removes a snapshot template from disk.
DeleteSnapshot(context.Context, *connect.Request[gen.DeleteSnapshotRequest]) (*connect.Response[gen.DeleteSnapshotResponse], error)
// ExecStream runs a command inside a sandbox and streams output events as they arrive.
ExecStream(context.Context, *connect.Request[gen.ExecStreamRequest]) (*connect.ServerStreamForClient[gen.ExecStreamResponse], error)
// WriteFileStream writes a file to a sandbox using chunked streaming.
@ -153,6 +164,18 @@ func NewHostAgentServiceClient(httpClient connect.HTTPClient, baseURL string, op
connect.WithSchema(hostAgentServiceMethods.ByName("ReadFile")),
connect.WithClientOptions(opts...),
),
createSnapshot: connect.NewClient[gen.CreateSnapshotRequest, gen.CreateSnapshotResponse](
httpClient,
baseURL+HostAgentServiceCreateSnapshotProcedure,
connect.WithSchema(hostAgentServiceMethods.ByName("CreateSnapshot")),
connect.WithClientOptions(opts...),
),
deleteSnapshot: connect.NewClient[gen.DeleteSnapshotRequest, gen.DeleteSnapshotResponse](
httpClient,
baseURL+HostAgentServiceDeleteSnapshotProcedure,
connect.WithSchema(hostAgentServiceMethods.ByName("DeleteSnapshot")),
connect.WithClientOptions(opts...),
),
execStream: connect.NewClient[gen.ExecStreamRequest, gen.ExecStreamResponse](
httpClient,
baseURL+HostAgentServiceExecStreamProcedure,
@ -184,6 +207,8 @@ type hostAgentServiceClient struct {
listSandboxes *connect.Client[gen.ListSandboxesRequest, gen.ListSandboxesResponse]
writeFile *connect.Client[gen.WriteFileRequest, gen.WriteFileResponse]
readFile *connect.Client[gen.ReadFileRequest, gen.ReadFileResponse]
createSnapshot *connect.Client[gen.CreateSnapshotRequest, gen.CreateSnapshotResponse]
deleteSnapshot *connect.Client[gen.DeleteSnapshotRequest, gen.DeleteSnapshotResponse]
execStream *connect.Client[gen.ExecStreamRequest, gen.ExecStreamResponse]
writeFileStream *connect.Client[gen.WriteFileStreamRequest, gen.WriteFileStreamResponse]
readFileStream *connect.Client[gen.ReadFileStreamRequest, gen.ReadFileStreamResponse]
@ -229,6 +254,16 @@ func (c *hostAgentServiceClient) ReadFile(ctx context.Context, req *connect.Requ
return c.readFile.CallUnary(ctx, req)
}
// CreateSnapshot calls hostagent.v1.HostAgentService.CreateSnapshot.
func (c *hostAgentServiceClient) CreateSnapshot(ctx context.Context, req *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) {
return c.createSnapshot.CallUnary(ctx, req)
}
// DeleteSnapshot calls hostagent.v1.HostAgentService.DeleteSnapshot.
func (c *hostAgentServiceClient) DeleteSnapshot(ctx context.Context, req *connect.Request[gen.DeleteSnapshotRequest]) (*connect.Response[gen.DeleteSnapshotResponse], error) {
return c.deleteSnapshot.CallUnary(ctx, req)
}
// ExecStream calls hostagent.v1.HostAgentService.ExecStream.
func (c *hostAgentServiceClient) ExecStream(ctx context.Context, req *connect.Request[gen.ExecStreamRequest]) (*connect.ServerStreamForClient[gen.ExecStreamResponse], error) {
return c.execStream.CallServerStream(ctx, req)
@ -262,6 +297,11 @@ type HostAgentServiceHandler interface {
WriteFile(context.Context, *connect.Request[gen.WriteFileRequest]) (*connect.Response[gen.WriteFileResponse], error)
// ReadFile reads a file from inside a sandbox.
ReadFile(context.Context, *connect.Request[gen.ReadFileRequest]) (*connect.Response[gen.ReadFileResponse], error)
// CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable
// template, and destroys the sandbox.
CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error)
// DeleteSnapshot removes a snapshot template from disk.
DeleteSnapshot(context.Context, *connect.Request[gen.DeleteSnapshotRequest]) (*connect.Response[gen.DeleteSnapshotResponse], error)
// ExecStream runs a command inside a sandbox and streams output events as they arrive.
ExecStream(context.Context, *connect.Request[gen.ExecStreamRequest], *connect.ServerStream[gen.ExecStreamResponse]) error
// WriteFileStream writes a file to a sandbox using chunked streaming.
@ -326,6 +366,18 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han
connect.WithSchema(hostAgentServiceMethods.ByName("ReadFile")),
connect.WithHandlerOptions(opts...),
)
hostAgentServiceCreateSnapshotHandler := connect.NewUnaryHandler(
HostAgentServiceCreateSnapshotProcedure,
svc.CreateSnapshot,
connect.WithSchema(hostAgentServiceMethods.ByName("CreateSnapshot")),
connect.WithHandlerOptions(opts...),
)
hostAgentServiceDeleteSnapshotHandler := connect.NewUnaryHandler(
HostAgentServiceDeleteSnapshotProcedure,
svc.DeleteSnapshot,
connect.WithSchema(hostAgentServiceMethods.ByName("DeleteSnapshot")),
connect.WithHandlerOptions(opts...),
)
hostAgentServiceExecStreamHandler := connect.NewServerStreamHandler(
HostAgentServiceExecStreamProcedure,
svc.ExecStream,
@ -362,6 +414,10 @@ func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.Han
hostAgentServiceWriteFileHandler.ServeHTTP(w, r)
case HostAgentServiceReadFileProcedure:
hostAgentServiceReadFileHandler.ServeHTTP(w, r)
case HostAgentServiceCreateSnapshotProcedure:
hostAgentServiceCreateSnapshotHandler.ServeHTTP(w, r)
case HostAgentServiceDeleteSnapshotProcedure:
hostAgentServiceDeleteSnapshotHandler.ServeHTTP(w, r)
case HostAgentServiceExecStreamProcedure:
hostAgentServiceExecStreamHandler.ServeHTTP(w, r)
case HostAgentServiceWriteFileStreamProcedure:
@ -409,6 +465,14 @@ func (UnimplementedHostAgentServiceHandler) ReadFile(context.Context, *connect.R
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ReadFile is not implemented"))
}
func (UnimplementedHostAgentServiceHandler) CreateSnapshot(context.Context, *connect.Request[gen.CreateSnapshotRequest]) (*connect.Response[gen.CreateSnapshotResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.CreateSnapshot is not implemented"))
}
func (UnimplementedHostAgentServiceHandler) DeleteSnapshot(context.Context, *connect.Request[gen.DeleteSnapshotRequest]) (*connect.Response[gen.DeleteSnapshotResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.DeleteSnapshot is not implemented"))
}
func (UnimplementedHostAgentServiceHandler) ExecStream(context.Context, *connect.Request[gen.ExecStreamRequest], *connect.ServerStream[gen.ExecStreamResponse]) error {
return connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ExecStream is not implemented"))
}

View File

@ -29,6 +29,13 @@ service HostAgentService {
// ReadFile reads a file from inside a sandbox.
rpc ReadFile(ReadFileRequest) returns (ReadFileResponse);
// CreateSnapshot pauses a sandbox, takes a snapshot, stores it as a reusable
// template, and destroys the sandbox.
rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
// DeleteSnapshot removes a snapshot template from disk.
rpc DeleteSnapshot(DeleteSnapshotRequest) returns (DeleteSnapshotResponse);
// ExecStream runs a command inside a sandbox and streams output events as they arrive.
rpc ExecStream(ExecStreamRequest) returns (stream ExecStreamResponse);
@ -80,7 +87,27 @@ message ResumeSandboxRequest {
string sandbox_id = 1;
}
message ResumeSandboxResponse {}
message ResumeSandboxResponse {
string sandbox_id = 1;
string status = 2;
string host_ip = 3;
}
message CreateSnapshotRequest {
string sandbox_id = 1;
string name = 2;
}
message CreateSnapshotResponse {
string name = 1;
int64 size_bytes = 2;
}
message DeleteSnapshotRequest {
string name = 1;
}
message DeleteSnapshotResponse {}
message ExecRequest {
string sandbox_id = 1;