diff --git a/.cursor/plans/systemd_vm_with_go_init_c0a9c010.plan.md b/.cursor/plans/systemd_vm_with_go_init_c0a9c010.plan.md new file mode 100644 index 00000000..f018b2dc --- /dev/null +++ b/.cursor/plans/systemd_vm_with_go_init_c0a9c010.plan.md @@ -0,0 +1,406 @@ +--- +name: Systemd VM with Go Init +overview: Support systemd-based OCI images via auto-detection from image CMD, using a Go-based init binary with human-readable logging, performing chroot for systemd handoff. +todos: + - id: go-init-scaffold + content: Create lib/system/init/ package with main.go entry point + status: completed + - id: go-init-mount + content: Implement mount.go for proc/sys/dev/overlay mounting + status: completed + - id: go-init-config + content: Implement config.go to parse config disk + status: completed + - id: go-init-network + content: Implement network.go for network configuration + status: completed + - id: go-init-drivers + content: Implement drivers.go for GPU driver loading + status: completed + - id: go-init-volumes + content: Implement volumes.go for volume mounting + status: completed + - id: go-init-exec + content: Implement mode_exec.go with current behavior + status: completed + - id: go-init-systemd + content: Implement mode_systemd.go with chroot and service injection + status: completed + - id: go-init-logger + content: Implement logger.go with human-readable format + status: completed + - id: initrd-build + content: Modify initrd.go to compile and include init binary + status: completed + - id: systemd-detect + content: Create lib/images/systemd.go with IsSystemdImage() based on CMD + status: completed + - id: config-disk-mode + content: Add INIT_MODE to configdisk.go based on CMD detection + status: completed + - id: agent-location + content: Change guest-agent copy location to /opt/hypeman/ + status: completed + - id: dialer-resilience + content: Add error handling for missing agent in client.go + status: completed + - id: test-dockerfile + content: Create integration/testdata/systemd/Dockerfile + status: completed + - id: e2e-tests + content: Create integration/systemd_test.go with build/push/test flow + status: completed +--- + +# Systemd VM Support with Go-based Init + +## Architecture + +```mermaid +flowchart TB + subgraph initrd [Initrd Contents] + GoInit[init Go binary] + BusyBox[busybox utilities] + GuestAgent[guest-agent binary] + end + + subgraph boot [Boot Flow] + Kernel[Kernel Boot] --> GoInit + GoInit --> MountFS[Mount proc/sys/dev] + MountFS --> Overlay[Setup overlay rootfs] + Overlay --> Config[Read config disk] + Config --> Network[Configure network] + Network --> Drivers[Load GPU drivers if needed] + Drivers --> Volumes[Mount volumes] + Volumes --> CopyAgent[Copy agent to /opt/hypeman/] + CopyAgent --> ModeCheck{init_mode from CMD?} + ModeCheck -->|exec| ExecMode[Exec Mode] + ModeCheck -->|systemd| SystemdMode[Systemd Mode] + end + + subgraph execpath [Exec Mode] + ExecMode --> StartAgentBG[Start guest-agent in background] + StartAgentBG --> RunEntrypoint[Run entrypoint as child] + RunEntrypoint --> WaitPID[Wait for entrypoint exit] + end + + subgraph systemdpath [Systemd Mode] + SystemdMode --> InjectService[Inject guest-agent.service] + InjectService --> Chroot[chroot to overlay/newroot] + Chroot --> ExecInit["exec /sbin/init (systemd)"] + ExecInit --> SystemdPID1[Systemd manages everything] + end +``` + +## Shared vs Mode-Specific Behavior + +| Step | Exec Mode | Systemd Mode ||------|-----------|--------------|| Mount proc/sys/dev | Shared | Shared || Mount rootfs overlay | Shared | Shared || Read config disk | Shared | Shared || Configure network | Init configures it | Init configures it (before pivot) || Load GPU drivers | Shared | Shared || Mount volumes | Shared | Shared || Copy guest-agent | To `/opt/hypeman/` | To `/opt/hypeman/` || Start guest-agent | Background process | Systemd service || PID 1 | Go init binary | Systemd || App lifecycle | Managed by init | Managed by systemd | + +## Logging Behavior + +### `hypeman logs` Output by Mode + +| Log Source | Exec Mode | Systemd Mode ||------------|-----------|--------------|| `--source app` (default) | Entrypoint stdout/stderr | Systemd boot messages + console output || `--source hypeman` | Init phases + operations | Init phases + operations (until pivot_root) || `--source vmm` | Cloud Hypervisor logs | Cloud Hypervisor logs |In systemd mode, after pivot_root: + +- Serial console (app.log) shows systemd boot progress and any services writing to console +- To view individual service logs, use: `hypeman exec journalctl -u ` +- To view guest-agent logs: `hypeman exec journalctl -u hypeman-agent` + +### Init Logger Format + +Human-readable format for `hypeman logs --source hypeman`: + +```go +// lib/system/init/logger.go +package main + +type Logger struct { + file *os.File +} + +func (l *Logger) Info(phase, msg string) { + // Format: 2024-12-23T10:15:30Z [INFO] [overlay] mounted rootfs from /dev/vda + fmt.Fprintf(l.file, "%s [INFO] [%s] %s\n", + time.Now().UTC().Format(time.RFC3339), phase, msg) +} + +func (l *Logger) Error(phase, msg string, err error) { + if err != nil { + fmt.Fprintf(l.file, "%s [ERROR] [%s] %s: %v\n", + time.Now().UTC().Format(time.RFC3339), phase, msg, err) + } else { + fmt.Fprintf(l.file, "%s [ERROR] [%s] %s\n", + time.Now().UTC().Format(time.RFC3339), phase, msg) + } +} + +// Example output: +// 2024-12-23T10:15:30Z [INFO] [boot] init starting +// 2024-12-23T10:15:30Z [INFO] [mount] mounted proc/sys/dev +// 2024-12-23T10:15:31Z [INFO] [overlay] mounted rootfs from /dev/vda +// 2024-12-23T10:15:31Z [INFO] [network] configured eth0 with 10.0.0.2/24 +// 2024-12-23T10:15:32Z [INFO] [systemd] performing pivot_root +// 2024-12-23T10:15:32Z [INFO] [systemd] exec /sbin/init +``` + +## Go-based Init Binary + +Package structure at `lib/system/init/`: + +```javascript +lib/system/init/ + main.go # Entry point, orchestrates boot + mount.go # Mount operations (proc, sys, dev, overlay) + config.go # Parse config disk + network.go # Network configuration + drivers.go # GPU driver loading + volumes.go # Volume mounting + mode_exec.go # Exec mode: run entrypoint + mode_systemd.go # Systemd mode: pivot_root + exec init + logger.go # Human-readable logging to hypeman operations log +``` + +### Main Orchestration + +```go +// lib/system/init/main.go +package main + +func main() { + log := NewLogger() + log.Info("boot", "init starting") + + if err := mountEssentials(log); err != nil { + log.Error("mount", "failed", err) + dropToShell() + } + + if err := setupOverlay(log); err != nil { + log.Error("overlay", "failed", err) + dropToShell() + } + + cfg, err := readConfig(log) + if err != nil { + log.Error("config", "failed", err) + dropToShell() + } + + if cfg.NetworkEnabled { + if err := configureNetwork(log, cfg); err != nil { + log.Error("network", "failed", err) + } + } + + if cfg.HasGPU { + if err := loadGPUDrivers(log); err != nil { + log.Error("gpu", "failed", err) + } + } + + if err := mountVolumes(log, cfg); err != nil { + log.Error("volumes", "failed", err) + } + + if err := copyGuestAgent(log); err != nil { + log.Error("agent", "failed to copy", err) + } + + if cfg.InitMode == "systemd" { + log.Info("mode", "entering systemd mode") + runSystemdMode(log, cfg) + } else { + log.Info("mode", "entering exec mode") + runExecMode(log, cfg) + } +} +``` + +### Systemd Mode + +```go +// lib/system/init/mode_systemd.go +package main + +import ( + "os" + "syscall" +) + +func runSystemdMode(log *Logger, cfg *Config) { + const newroot = "/overlay/newroot" + + log.Info("systemd", "injecting hypeman-agent.service") + if err := injectAgentService(newroot); err != nil { + log.Error("systemd", "failed to inject service", err) + } + + // Use chroot instead of pivot_root (more reliable in VM environment) + log.Info("systemd", "executing chroot") + if err := syscall.Chroot(newroot); err != nil { + log.Error("systemd", "chroot failed", err) + dropToShell() + } + + os.Chdir("/") + + log.Info("systemd", "exec /sbin/init") + syscall.Exec("/sbin/init", []string{"/sbin/init"}, os.Environ()) + + log.Error("systemd", "exec failed", nil) + dropToShell() +} + +func injectAgentService(newroot string) error { + serviceContent := `[Unit] +Description=Hypeman Guest Agent +After=network.target + +[Service] +Type=simple +ExecStart=/opt/hypeman/guest-agent +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +` + serviceDir := newroot + "/etc/systemd/system" + wantsDir := serviceDir + "/multi-user.target.wants" + os.MkdirAll(wantsDir, 0755) + + servicePath := serviceDir + "/hypeman-agent.service" + if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil { + return err + } + return os.Symlink(servicePath, wantsDir+"/hypeman-agent.service") +} +``` + +## Detection Logic + +Auto-detect systemd mode by inspecting the image's CMD. No override flag - if CMD is a systemd init, always use systemd mode. + +```go +// lib/images/systemd.go +package images + +import "strings" + +// IsSystemdImage checks if the image's CMD indicates it wants systemd as init. +// Detection is based on the effective command (entrypoint + cmd), not whether +// systemd is installed in the image. +func IsSystemdImage(entrypoint, cmd []string) bool { + // Combine to get the actual command that will run + effective := append(entrypoint, cmd...) + if len(effective) == 0 { + return false + } + + first := effective[0] + + // Match specific systemd/init paths + systemdPaths := []string{ + "/sbin/init", + "/lib/systemd/systemd", + "/usr/lib/systemd/systemd", + } + for _, p := range systemdPaths { + if first == p { + return true + } + } + + // Match any path ending in /init (e.g., /usr/sbin/init) + if strings.HasSuffix(first, "/init") { + return true + } + + return false +} +``` + +## E2E Test + +Custom Dockerfile in repository at `integration/testdata/systemd/Dockerfile`: + +```dockerfile +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + systemd \ + systemd-sysv \ + dbus \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Remove unnecessary systemd units +RUN rm -f /lib/systemd/system/multi-user.target.wants/* \ + /etc/systemd/system/*.wants/* \ + /lib/systemd/system/local-fs.target.wants/* \ + /lib/systemd/system/sockets.target.wants/*udev* \ + /lib/systemd/system/sockets.target.wants/*initctl* + +VOLUME ["/sys/fs/cgroup"] +CMD ["/lib/systemd/systemd"] +``` + +Test flow: + +1. Build image with `docker build` +2. Push to hypeman via OCI import +3. Run instance (auto-detects systemd mode from CMD) +4. Verify systemd is PID 1 +5. Verify guest-agent.service is active +6. Verify `hypeman logs` shows systemd boot messages +```go +// integration/systemd_test.go + +func TestSystemdMode(t *testing.T) { + // Build and push test image + buildAndPushTestImage(t, "integration/testdata/systemd", "test-systemd:latest") + + // Create instance (auto-detects systemd mode from CMD) + inst := createInstance(t, "test-systemd:latest") + defer deleteInstance(t, inst.Id) + + time.Sleep(10 * time.Second) + + // Verify systemd is PID 1 + result := execInVM(t, inst, "cat", "/proc/1/comm") + assert.Equal(t, "systemd", strings.TrimSpace(result.Stdout)) + + // Verify agent service is running + result = execInVM(t, inst, "systemctl", "is-active", "hypeman-agent") + assert.Equal(t, "active", strings.TrimSpace(result.Stdout)) + + // Verify agent location + result = execInVM(t, inst, "test", "-x", "/opt/hypeman/guest-agent") + assert.Equal(t, 0, result.ExitCode) + + // Verify can view agent logs via journalctl + result = execInVM(t, inst, "journalctl", "-u", "hypeman-agent", "--no-pager") + assert.Equal(t, 0, result.ExitCode) +} + +func TestExecModeUnchanged(t *testing.T) { + // Regular container image should still work as before + inst := createInstance(t, "nginx:alpine") + defer deleteInstance(t, inst.Id) + + time.Sleep(3 * time.Second) + + // Nginx should be running + result := execInVM(t, inst, "pgrep", "nginx") + assert.Equal(t, 0, result.ExitCode) + + // PID 1 is init binary (not systemd) + result = execInVM(t, inst, "cat", "/proc/1/comm") + assert.Equal(t, "init", strings.TrimSpace(result.Stdout)) +} +``` + + +## Files to Modify/Create \ No newline at end of file diff --git a/.gitignore b/.gitignore index e76a3b0a..b2b815d4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ cloud-hypervisor cloud-hypervisor/** lib/system/exec_agent/exec-agent lib/system/guest_agent/guest-agent -lib/system/guest_agent/guest_agent +lib/system/init/init # Envoy binaries lib/ingress/binaries/** diff --git a/Makefile b/Makefile index 4925096f..915a91a0 100644 --- a/Makefile +++ b/Makefile @@ -122,15 +122,16 @@ generate-wire: $(WIRE) @echo "Generating wire code..." cd ./cmd/api && $(WIRE) -# Generate gRPC code from proto -generate-grpc: - @echo "Generating gRPC code from proto..." - protoc --go_out=. --go_opt=paths=source_relative \ - --go-grpc_out=. --go-grpc_opt=paths=source_relative \ +# Generate DRPC code from proto +# Note: PATH ordering ensures we use GOPATH/bin versions of protoc-gen-go and protoc-gen-go-drpc +generate-drpc: + @echo "Generating DRPC code from proto..." + PATH=$(shell go env GOPATH)/bin:$$PATH protoc --go_out=. --go_opt=paths=source_relative \ + --go-drpc_out=. --go-drpc_opt=paths=source_relative \ lib/guest/guest.proto # Generate all code -generate-all: oapi-generate generate-vmm-client generate-wire generate-grpc +generate-all: oapi-generate generate-vmm-client generate-wire generate-drpc # Check if CH binaries exist, download if missing .PHONY: ensure-ch-binaries @@ -165,26 +166,36 @@ ensure-caddy-binaries: fi # Build guest-agent (guest binary) into its own directory for embedding -lib/system/guest_agent/guest-agent: lib/system/guest_agent/main.go +lib/system/guest_agent/guest-agent: lib/system/guest_agent/*.go @echo "Building guest-agent..." cd lib/system/guest_agent && CGO_ENABLED=0 go build -ldflags="-s -w" -o guest-agent . +# Build init binary (runs as PID 1 in guest VM) for embedding +# Uses static linking for portability across different guest environments +lib/system/init/init: lib/system/init/*.go + @echo "Building init binary..." + cd lib/system/init && CGO_ENABLED=0 go build -ldflags="-s -w" -o init . + +# Build all embedded binaries +.PHONY: build-embedded +build-embedded: lib/system/guest_agent/guest-agent lib/system/init/init + # Build the binary -build: ensure-ch-binaries ensure-caddy-binaries lib/system/guest_agent/guest-agent | $(BIN_DIR) +build: ensure-ch-binaries ensure-caddy-binaries build-embedded | $(BIN_DIR) go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api # Build all binaries build-all: build # Run in development mode with hot reload -dev: ensure-ch-binaries ensure-caddy-binaries lib/system/guest_agent/guest-agent $(AIR) +dev: ensure-ch-binaries ensure-caddy-binaries build-embedded $(AIR) @rm -f ./tmp/main $(AIR) -c .air.toml # Run tests (as root for network capabilities, enables caching and parallelism) # Usage: make test - runs all tests # make test TEST=TestCreateInstanceWithNetwork - runs specific test -test: ensure-ch-binaries ensure-caddy-binaries lib/system/guest_agent/guest-agent +test: ensure-ch-binaries ensure-caddy-binaries build-embedded @if [ -n "$(TEST)" ]; then \ echo "Running specific test: $(TEST)"; \ sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp -run=$(TEST) -v -timeout=180s ./...; \ @@ -203,8 +214,9 @@ clean: rm -rf lib/vmm/binaries/cloud-hypervisor/ rm -rf lib/ingress/binaries/ rm -f lib/system/guest_agent/guest-agent + rm -f lib/system/init/init # Prepare for release build (called by GoReleaser) # Downloads all embedded binaries and builds embedded components -release-prep: download-ch-binaries build-caddy-binaries lib/system/guest_agent/guest-agent +release-prep: download-ch-binaries build-caddy-binaries build-embedded go mod tidy diff --git a/cmd/api/api/cp.go b/cmd/api/api/cp.go index 89b40a8b..92a3c870 100644 --- a/cmd/api/api/cp.go +++ b/cmd/api/api/cp.go @@ -230,7 +230,7 @@ func (s *ApiService) handleCopyTo(ctx context.Context, ws *websocket.Conn, inst return 0, fmt.Errorf("get grpc connection: %w", err) } - client := guest.NewGuestServiceClient(grpcConn) + client := guest.NewDRPCGuestServiceClient(grpcConn) stream, err := client.CopyToGuest(ctx) if err != nil { return 0, fmt.Errorf("start copy stream: %w", err) @@ -340,7 +340,7 @@ func (s *ApiService) handleCopyFrom(ctx context.Context, ws *websocket.Conn, ins return 0, fmt.Errorf("get grpc connection: %w", err) } - client := guest.NewGuestServiceClient(grpcConn) + client := guest.NewDRPCGuestServiceClient(grpcConn) stream, err := client.CopyFromGuest(ctx, &guest.CopyFromGuestRequest{ Path: req.GuestPath, FollowLinks: req.FollowLinks, diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index 69968b0c..c7ec869d 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -479,7 +479,7 @@ func (s *ApiService) StatInstancePath(ctx context.Context, request oapi.StatInst }, nil } - client := guest.NewGuestServiceClient(grpcConn) + client := guest.NewDRPCGuestServiceClient(grpcConn) followLinks := false if request.Params.FollowLinks != nil { followLinks = *request.Params.FollowLinks diff --git a/go.mod b/go.mod index 830eb39e..af2fb405 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-chi/chi/v5 v5.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/golang/protobuf v1.5.4 github.com/google/go-containerregistry v0.20.6 github.com/google/wire v0.7.0 github.com/gorilla/websocket v1.5.3 @@ -43,8 +42,9 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 golang.org/x/sync v0.17.0 golang.org/x/sys v0.38.0 - google.golang.org/grpc v1.77.0 + google.golang.org/protobuf v1.36.10 gvisor.dev/gvisor v0.0.0-20251125014920-fc40e232ff54 + storj.io/drpc v0.0.34 ) require ( @@ -102,6 +102,7 @@ require ( github.com/vbatts/tar-split v0.12.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/zeebo/errs v1.2.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect @@ -114,7 +115,7 @@ require ( golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/grpc v1.77.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect diff --git a/go.sum b/go.sum index 3edd3725..0a11e855 100644 --- a/go.sum +++ b/go.sum @@ -238,6 +238,10 @@ github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4Z github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/errs v1.2.2 h1:5NFypMTuSdoySVTqlNs1dEoU21QVamMQJxW/Fii5O7g= +github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s= @@ -365,3 +369,5 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gvisor.dev/gvisor v0.0.0-20251125014920-fc40e232ff54 h1:eYMn6Z3T40m4f9vVYRcsjvX4eEv7ng7FgrZTbadSyBs= gvisor.dev/gvisor v0.0.0-20251125014920-fc40e232ff54/go.mod h1:W1ZgZ/Dh85TgSZWH67l2jKVpDE5bjIaut7rjwwOiHzQ= +storj.io/drpc v0.0.34 h1:q9zlQKfJ5A7x8NQNFk8x7eKUF78FMhmAbZLnFK+og7I= +storj.io/drpc v0.0.34/go.mod h1:Y9LZaa8esL1PW2IDMqJE7CFSNq7d5bQ3RI7mGPtmKMg= diff --git a/integration/systemd_test.go b/integration/systemd_test.go new file mode 100644 index 00000000..760620c0 --- /dev/null +++ b/integration/systemd_test.go @@ -0,0 +1,223 @@ +package integration + +import ( + "bytes" + "context" + "os" + "strings" + "testing" + "time" + + "github.com/onkernel/hypeman/cmd/api/config" + "github.com/onkernel/hypeman/lib/devices" + "github.com/onkernel/hypeman/lib/guest" + "github.com/onkernel/hypeman/lib/hypervisor" + "github.com/onkernel/hypeman/lib/images" + "github.com/onkernel/hypeman/lib/instances" + "github.com/onkernel/hypeman/lib/network" + "github.com/onkernel/hypeman/lib/paths" + "github.com/onkernel/hypeman/lib/system" + "github.com/onkernel/hypeman/lib/volumes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSystemdMode verifies that hypeman correctly detects and runs +// systemd-based images with systemd as PID 1. +// +// This test uses the jrei/systemd-ubuntu image from Docker Hub which runs +// systemd as its CMD. The test verifies that hypeman auto-detects this and: +// - Uses systemd mode (chroot to container rootfs) +// - Starts systemd as PID 1 +// - Injects and starts the hypeman-agent.service +func TestSystemdMode(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + // Skip if KVM is not available + if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) { + t.Skip("/dev/kvm not available") + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // Set up test environment + tmpDir := t.TempDir() + p := paths.New(tmpDir) + + cfg := &config.Config{ + DataDir: tmpDir, + BridgeName: "vmbr0", + SubnetCIDR: "10.100.0.0/16", + DNSServer: "1.1.1.1", + } + + // Create managers + imageManager, err := images.NewManager(p, 1, nil) + require.NoError(t, err) + + systemManager := system.NewManager(p) + networkManager := network.NewManager(p, cfg, nil) + deviceManager := devices.NewManager(p) + volumeManager := volumes.NewManager(p, 0, nil) + + limits := instances.ResourceLimits{ + MaxOverlaySize: 100 * 1024 * 1024 * 1024, + MaxVcpusPerInstance: 0, + MaxMemoryPerInstance: 0, + MaxTotalVcpus: 0, + MaxTotalMemory: 0, + } + + instanceManager := instances.NewManager(p, imageManager, systemManager, networkManager, deviceManager, volumeManager, limits, "", nil, nil) + + // Cleanup any orphaned instances + t.Cleanup(func() { + instanceManager.DeleteInstance(ctx, "systemd-test") + }) + + imageName := "docker.io/jrei/systemd-ubuntu:22.04" + + // Pull the systemd image + t.Log("Pulling systemd image:", imageName) + _, err = imageManager.CreateImage(ctx, images.CreateImageRequest{ + Name: imageName, + }) + require.NoError(t, err) + + // Wait for image to be ready + t.Log("Waiting for image build...") + var img *images.Image + for i := 0; i < 120; i++ { + img, err = imageManager.GetImage(ctx, imageName) + if err == nil && img.Status == images.StatusReady { + break + } + time.Sleep(1 * time.Second) + } + require.Equal(t, images.StatusReady, img.Status, "image should be ready") + + // Verify systemd detection + t.Run("IsSystemdImage", func(t *testing.T) { + isSystemd := images.IsSystemdImage(img.Entrypoint, img.Cmd) + assert.True(t, isSystemd, "image should be detected as systemd, entrypoint=%v cmd=%v", img.Entrypoint, img.Cmd) + }) + + // Ensure system files (kernel, initrd) + t.Log("Ensuring system files...") + err = systemManager.EnsureSystemFiles(ctx) + require.NoError(t, err) + + // Create the systemd instance + t.Log("Creating systemd instance...") + inst, err := instanceManager.CreateInstance(ctx, instances.CreateInstanceRequest{ + Name: "systemd-test", + Image: imageName, + Size: 2 * 1024 * 1024 * 1024, // 2GB + HotplugSize: 512 * 1024 * 1024, + OverlaySize: 1024 * 1024 * 1024, + Vcpus: 2, + NetworkEnabled: false, // No network needed for this test + }) + require.NoError(t, err) + t.Logf("Instance created: %s", inst.Id) + + // Wait for guest agent to be ready + t.Log("Waiting for guest agent...") + err = waitForGuestAgent(ctx, instanceManager, inst.Id, 60*time.Second) + require.NoError(t, err, "guest agent should be ready") + + // Test: Verify systemd is PID 1 + t.Run("SystemdIsPID1", func(t *testing.T) { + output, exitCode, err := execInInstance(ctx, inst, "cat", "/proc/1/comm") + require.NoError(t, err, "exec should work") + require.Equal(t, 0, exitCode, "command should succeed") + + pid1Name := strings.TrimSpace(output) + assert.Equal(t, "systemd", pid1Name, "PID 1 should be systemd") + t.Logf("PID 1 is: %s", pid1Name) + }) + + // Test: Verify guest-agent binary exists + t.Run("GuestAgentExists", func(t *testing.T) { + output, exitCode, err := execInInstance(ctx, inst, "test", "-x", "/opt/hypeman/guest-agent") + require.NoError(t, err, "exec should work") + assert.Equal(t, 0, exitCode, "guest-agent binary should exist at /opt/hypeman/guest-agent, output: %s", output) + }) + + // Test: Verify hypeman-agent.service is active + t.Run("AgentServiceActive", func(t *testing.T) { + output, exitCode, err := execInInstance(ctx, inst, "systemctl", "is-active", "hypeman-agent") + require.NoError(t, err, "exec should work") + status := strings.TrimSpace(output) + assert.Equal(t, 0, exitCode, "hypeman-agent service should be active, status: %s", status) + assert.Equal(t, "active", status, "service status should be 'active'") + t.Logf("hypeman-agent service status: %s", status) + }) + + // Test: Verify we can view agent logs via journalctl + t.Run("AgentLogsAccessible", func(t *testing.T) { + output, exitCode, err := execInInstance(ctx, inst, "journalctl", "-u", "hypeman-agent", "--no-pager", "-n", "5") + require.NoError(t, err, "exec should work") + assert.Equal(t, 0, exitCode, "journalctl should succeed") + t.Logf("Agent logs (last 5 lines):\n%s", output) + }) + + t.Log("All systemd mode tests passed!") +} + +// waitForGuestAgent polls until the guest agent is ready +func waitForGuestAgent(ctx context.Context, mgr instances.Manager, instanceID string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + inst, err := mgr.GetInstance(ctx, instanceID) + if err != nil { + time.Sleep(500 * time.Millisecond) + continue + } + + // Try to connect to the guest agent + dialer, err := hypervisor.NewVsockDialer(inst.HypervisorType, inst.VsockSocket, inst.VsockCID) + if err != nil { + time.Sleep(500 * time.Millisecond) + continue + } + + // Try a simple exec to verify agent is responding + var stdout bytes.Buffer + _, err = guest.ExecIntoInstance(ctx, dialer, guest.ExecOptions{ + Command: []string{"echo", "ready"}, + Stdout: &stdout, + TTY: false, + }) + if err == nil { + return nil + } + + time.Sleep(500 * time.Millisecond) + } + return context.DeadlineExceeded +} + +// execInInstance executes a command in the instance +func execInInstance(ctx context.Context, inst *instances.Instance, command ...string) (string, int, error) { + dialer, err := hypervisor.NewVsockDialer(inst.HypervisorType, inst.VsockSocket, inst.VsockCID) + if err != nil { + return "", -1, err + } + + var stdout, stderr bytes.Buffer + exit, err := guest.ExecIntoInstance(ctx, dialer, guest.ExecOptions{ + Command: command, + Stdout: &stdout, + Stderr: &stderr, + TTY: false, + }) + if err != nil { + return stderr.String(), -1, err + } + + return stdout.String(), exit.Code, nil +} diff --git a/lib/guest/client.go b/lib/guest/client.go index dc7a7fd3..43de0241 100644 --- a/lib/guest/client.go +++ b/lib/guest/client.go @@ -2,6 +2,7 @@ package guest import ( "context" + "errors" "fmt" "io" "io/fs" @@ -16,8 +17,7 @@ import ( securejoin "github.com/cyphar/filepath-securejoin" "github.com/onkernel/hypeman/lib/hypervisor" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "storj.io/drpc/drpcconn" ) const ( @@ -25,25 +25,56 @@ const ( vsockGuestPort = 2222 ) -// connPool manages reusable gRPC connections per vsock dialer key +// AgentConnectionError indicates the guest agent is not responding. +// This can happen if: +// - The VM is still booting +// - The guest agent was stopped or deleted +// - The VM is in systemd mode and the agent service failed to start +type AgentConnectionError struct { + Err error +} + +func (e *AgentConnectionError) Error() string { + return fmt.Sprintf("guest agent not responding (it may have been stopped, deleted, or the VM is still booting): %v", e.Err) +} + +func (e *AgentConnectionError) Unwrap() error { + return e.Err +} + +// IsAgentConnectionError checks if an error is due to the guest agent not responding. +func IsAgentConnectionError(err error) bool { + var agentErr *AgentConnectionError + return err != nil && (strings.Contains(err.Error(), "guest agent not responding") || + strings.Contains(err.Error(), "connection refused") || + errors.As(err, &agentErr)) +} + +// pooledConn wraps a drpc connection with its underlying net.Conn for cleanup +type pooledConn struct { + drpc *drpcconn.Conn + netConn net.Conn +} + +// connPool manages reusable DRPC connections per vsock dialer key // This avoids the overhead and potential issues of rapidly creating/closing connections var connPool = struct { sync.RWMutex - conns map[string]*grpc.ClientConn + conns map[string]*pooledConn }{ - conns: make(map[string]*grpc.ClientConn), + conns: make(map[string]*pooledConn), } // GetOrCreateConn returns an existing connection or creates a new one using a VsockDialer. // This supports multiple hypervisor types (Cloud Hypervisor, QEMU, etc.). -func GetOrCreateConn(ctx context.Context, dialer hypervisor.VsockDialer) (*grpc.ClientConn, error) { +func GetOrCreateConn(ctx context.Context, dialer hypervisor.VsockDialer) (*drpcconn.Conn, error) { key := dialer.Key() // Try read lock first for existing connection connPool.RLock() - if conn, ok := connPool.conns[key]; ok { + if pc, ok := connPool.conns[key]; ok { connPool.RUnlock() - return conn, nil + return pc.drpc, nil } connPool.RUnlock() @@ -52,36 +83,33 @@ func GetOrCreateConn(ctx context.Context, dialer hypervisor.VsockDialer) (*grpc. defer connPool.Unlock() // Double-check after acquiring write lock - if conn, ok := connPool.conns[key]; ok { - return conn, nil + if pc, ok := connPool.conns[key]; ok { + return pc.drpc, nil } // Create new connection using the VsockDialer - conn, err := grpc.Dial("passthrough:///vsock", - grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { - return dialer.DialVsock(ctx, vsockGuestPort) - }), - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) + netConn, err := dialer.DialVsock(ctx, vsockGuestPort) if err != nil { - return nil, fmt.Errorf("create grpc connection: %w", err) + return nil, &AgentConnectionError{Err: err} } - connPool.conns[key] = conn - slog.Debug("created new gRPC connection", "key", key) + // Wrap in drpc connection + conn := drpcconn.New(netConn) + + connPool.conns[key] = &pooledConn{drpc: conn, netConn: netConn} + slog.Debug("created new DRPC connection", "key", key) return conn, nil } // CloseConn removes a connection from the pool by key (call when VM is deleted). -// We only remove from pool, not explicitly close - the connection will fail -// naturally when the VM dies, and grpc will clean up. func CloseConn(dialerKey string) { connPool.Lock() defer connPool.Unlock() - if _, ok := connPool.conns[dialerKey]; ok { + if pc, ok := connPool.conns[dialerKey]; ok { + pc.drpc.Close() delete(connPool.conns, dialerKey) - slog.Debug("removed gRPC connection from pool", "key", dialerKey) + slog.Debug("removed DRPC connection from pool", "key", dialerKey) } } @@ -102,27 +130,27 @@ type ExecOptions struct { Timeout int32 // Execution timeout in seconds (0 = no timeout) } -// ExecIntoInstance executes command in instance via vsock using gRPC. +// ExecIntoInstance executes command in instance via vsock using DRPC. // The dialer is a hypervisor-specific VsockDialer that knows how to connect to the guest. func ExecIntoInstance(ctx context.Context, dialer hypervisor.VsockDialer, opts ExecOptions) (*ExitStatus, error) { start := time.Now() var bytesSent int64 - // Get or create a reusable gRPC connection for this vsock dialer - grpcConn, err := GetOrCreateConn(ctx, dialer) + // Get or create a reusable DRPC connection for this vsock dialer + drpcConn, err := GetOrCreateConn(ctx, dialer) if err != nil { - return nil, fmt.Errorf("get grpc connection: %w", err) + return nil, fmt.Errorf("get drpc connection: %w", err) } // Note: Don't close the connection - it's pooled and reused // Create guest client - client := NewGuestServiceClient(grpcConn) + client := NewDRPCGuestServiceClient(drpcConn) stream, err := client.Exec(ctx) if err != nil { return nil, fmt.Errorf("start exec stream: %w", err) } // Ensure stream is properly closed when we're done - defer stream.CloseSend() + defer stream.Close() // Send start request if err := stream.Send(&ExecRequest{ @@ -203,12 +231,12 @@ type CopyToInstanceOptions struct { // CopyToInstance copies a file or directory to an instance via vsock. // The dialer is a hypervisor-specific VsockDialer that knows how to connect to the guest. func CopyToInstance(ctx context.Context, dialer hypervisor.VsockDialer, opts CopyToInstanceOptions) error { - grpcConn, err := GetOrCreateConn(ctx, dialer) + drpcConn, err := GetOrCreateConn(ctx, dialer) if err != nil { - return fmt.Errorf("get grpc connection: %w", err) + return fmt.Errorf("get drpc connection: %w", err) } - client := NewGuestServiceClient(grpcConn) + client := NewDRPCGuestServiceClient(drpcConn) // Stat the source srcInfo, err := os.Stat(opts.SrcPath) @@ -223,7 +251,7 @@ func CopyToInstance(ctx context.Context, dialer hypervisor.VsockDialer, opts Cop } // copyFileToInstance copies a single file to the instance -func copyFileToInstance(ctx context.Context, client GuestServiceClient, srcPath, dstPath string, mode fs.FileMode) error { +func copyFileToInstance(ctx context.Context, client DRPCGuestServiceClient, srcPath, dstPath string, mode fs.FileMode) error { srcInfo, err := os.Stat(srcPath) if err != nil { return fmt.Errorf("stat source: %w", err) @@ -299,7 +327,7 @@ func copyFileToInstance(ctx context.Context, client GuestServiceClient, srcPath, } // copyDirToInstance copies a directory recursively to the instance -func copyDirToInstance(ctx context.Context, client GuestServiceClient, srcPath, dstPath string) error { +func copyDirToInstance(ctx context.Context, client DRPCGuestServiceClient, srcPath, dstPath string) error { srcPath = filepath.Clean(srcPath) // First create the destination directory @@ -414,12 +442,12 @@ type FileHandler func(header *CopyFromGuestHeader, data io.Reader) error // CopyFromInstance copies a file or directory from an instance via vsock. // The dialer is a hypervisor-specific VsockDialer that knows how to connect to the guest. func CopyFromInstance(ctx context.Context, dialer hypervisor.VsockDialer, opts CopyFromInstanceOptions) error { - grpcConn, err := GetOrCreateConn(ctx, dialer) + drpcConn, err := GetOrCreateConn(ctx, dialer) if err != nil { - return fmt.Errorf("get grpc connection: %w", err) + return fmt.Errorf("get drpc connection: %w", err) } - client := NewGuestServiceClient(grpcConn) + client := NewDRPCGuestServiceClient(drpcConn) stream, err := client.CopyFromGuest(ctx, &CopyFromGuestRequest{ Path: opts.SrcPath, diff --git a/lib/guest/guest.pb.go b/lib/guest/guest.pb.go index 80549077..d78dd89f 100644 --- a/lib/guest/guest.pb.go +++ b/lib/guest/guest.pb.go @@ -1,231 +1,277 @@ // Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 // source: lib/guest/guest.proto package guest import ( - fmt "fmt" - proto "github.com/golang/protobuf/proto" - math "math" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) // ExecRequest represents messages from client to server type ExecRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Request: // // *ExecRequest_Start // *ExecRequest_Stdin - Request isExecRequest_Request `protobuf_oneof:"request"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Request isExecRequest_Request `protobuf_oneof:"request"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *ExecRequest) Reset() { *m = ExecRequest{} } -func (m *ExecRequest) String() string { return proto.CompactTextString(m) } -func (*ExecRequest) ProtoMessage() {} -func (*ExecRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{0} +func (x *ExecRequest) Reset() { + *x = ExecRequest{} + mi := &file_lib_guest_guest_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *ExecRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExecRequest.Unmarshal(m, b) +func (x *ExecRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *ExecRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExecRequest.Marshal(b, m, deterministic) + +func (*ExecRequest) ProtoMessage() {} + +func (x *ExecRequest) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -func (m *ExecRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExecRequest.Merge(m, src) + +// Deprecated: Use ExecRequest.ProtoReflect.Descriptor instead. +func (*ExecRequest) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{0} } -func (m *ExecRequest) XXX_Size() int { - return xxx_messageInfo_ExecRequest.Size(m) + +func (x *ExecRequest) GetRequest() isExecRequest_Request { + if x != nil { + return x.Request + } + return nil } -func (m *ExecRequest) XXX_DiscardUnknown() { - xxx_messageInfo_ExecRequest.DiscardUnknown(m) + +func (x *ExecRequest) GetStart() *ExecStart { + if x != nil { + if x, ok := x.Request.(*ExecRequest_Start); ok { + return x.Start + } + } + return nil } -var xxx_messageInfo_ExecRequest proto.InternalMessageInfo +func (x *ExecRequest) GetStdin() []byte { + if x != nil { + if x, ok := x.Request.(*ExecRequest_Stdin); ok { + return x.Stdin + } + } + return nil +} type isExecRequest_Request interface { isExecRequest_Request() } type ExecRequest_Start struct { - Start *ExecStart `protobuf:"bytes,1,opt,name=start,proto3,oneof"` + Start *ExecStart `protobuf:"bytes,1,opt,name=start,proto3,oneof"` // Initial exec request } type ExecRequest_Stdin struct { - Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"` + Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"` // Stdin data } func (*ExecRequest_Start) isExecRequest_Request() {} func (*ExecRequest_Stdin) isExecRequest_Request() {} -func (m *ExecRequest) GetRequest() isExecRequest_Request { - if m != nil { - return m.Request - } - return nil +// ExecStart initiates command execution +type ExecStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + Command []string `protobuf:"bytes,1,rep,name=command,proto3" json:"command,omitempty"` // Command and arguments + Tty bool `protobuf:"varint,2,opt,name=tty,proto3" json:"tty,omitempty"` // Allocate pseudo-TTY + Env map[string]string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Environment variables + Cwd string `protobuf:"bytes,4,opt,name=cwd,proto3" json:"cwd,omitempty"` // Working directory (optional) + TimeoutSeconds int32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` // Execution timeout in seconds (0 = no timeout) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *ExecRequest) GetStart() *ExecStart { - if x, ok := m.GetRequest().(*ExecRequest_Start); ok { - return x.Start - } - return nil +func (x *ExecStart) Reset() { + *x = ExecStart{} + mi := &file_lib_guest_guest_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *ExecRequest) GetStdin() []byte { - if x, ok := m.GetRequest().(*ExecRequest_Stdin); ok { - return x.Stdin - } - return nil +func (x *ExecStart) String() string { + return protoimpl.X.MessageStringOf(x) } -// XXX_OneofWrappers is for the internal use of the proto package. -func (*ExecRequest) XXX_OneofWrappers() []interface{} { - return []interface{}{ - (*ExecRequest_Start)(nil), - (*ExecRequest_Stdin)(nil), +func (*ExecStart) ProtoMessage() {} + +func (x *ExecStart) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } + return mi.MessageOf(x) } -// ExecStart initiates command execution -type ExecStart struct { - Command []string `protobuf:"bytes,1,rep,name=command,proto3" json:"command,omitempty"` - Tty bool `protobuf:"varint,2,opt,name=tty,proto3" json:"tty,omitempty"` - Env map[string]string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - Cwd string `protobuf:"bytes,4,opt,name=cwd,proto3" json:"cwd,omitempty"` - TimeoutSeconds int32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *ExecStart) Reset() { *m = ExecStart{} } -func (m *ExecStart) String() string { return proto.CompactTextString(m) } -func (*ExecStart) ProtoMessage() {} +// Deprecated: Use ExecStart.ProtoReflect.Descriptor instead. func (*ExecStart) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{1} -} - -func (m *ExecStart) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExecStart.Unmarshal(m, b) -} -func (m *ExecStart) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExecStart.Marshal(b, m, deterministic) -} -func (m *ExecStart) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExecStart.Merge(m, src) + return file_lib_guest_guest_proto_rawDescGZIP(), []int{1} } -func (m *ExecStart) XXX_Size() int { - return xxx_messageInfo_ExecStart.Size(m) -} -func (m *ExecStart) XXX_DiscardUnknown() { - xxx_messageInfo_ExecStart.DiscardUnknown(m) -} - -var xxx_messageInfo_ExecStart proto.InternalMessageInfo -func (m *ExecStart) GetCommand() []string { - if m != nil { - return m.Command +func (x *ExecStart) GetCommand() []string { + if x != nil { + return x.Command } return nil } -func (m *ExecStart) GetTty() bool { - if m != nil { - return m.Tty +func (x *ExecStart) GetTty() bool { + if x != nil { + return x.Tty } return false } -func (m *ExecStart) GetEnv() map[string]string { - if m != nil { - return m.Env +func (x *ExecStart) GetEnv() map[string]string { + if x != nil { + return x.Env } return nil } -func (m *ExecStart) GetCwd() string { - if m != nil { - return m.Cwd +func (x *ExecStart) GetCwd() string { + if x != nil { + return x.Cwd } return "" } -func (m *ExecStart) GetTimeoutSeconds() int32 { - if m != nil { - return m.TimeoutSeconds +func (x *ExecStart) GetTimeoutSeconds() int32 { + if x != nil { + return x.TimeoutSeconds } return 0 } // ExecResponse represents messages from server to client type ExecResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Response: // // *ExecResponse_Stdout // *ExecResponse_Stderr // *ExecResponse_ExitCode - Response isExecResponse_Response `protobuf_oneof:"response"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Response isExecResponse_Response `protobuf_oneof:"response"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *ExecResponse) Reset() { *m = ExecResponse{} } -func (m *ExecResponse) String() string { return proto.CompactTextString(m) } -func (*ExecResponse) ProtoMessage() {} -func (*ExecResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{2} +func (x *ExecResponse) Reset() { + *x = ExecResponse{} + mi := &file_lib_guest_guest_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *ExecResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_ExecResponse.Unmarshal(m, b) +func (x *ExecResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *ExecResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_ExecResponse.Marshal(b, m, deterministic) + +func (*ExecResponse) ProtoMessage() {} + +func (x *ExecResponse) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -func (m *ExecResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExecResponse.Merge(m, src) + +// Deprecated: Use ExecResponse.ProtoReflect.Descriptor instead. +func (*ExecResponse) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{2} } -func (m *ExecResponse) XXX_Size() int { - return xxx_messageInfo_ExecResponse.Size(m) + +func (x *ExecResponse) GetResponse() isExecResponse_Response { + if x != nil { + return x.Response + } + return nil +} + +func (x *ExecResponse) GetStdout() []byte { + if x != nil { + if x, ok := x.Response.(*ExecResponse_Stdout); ok { + return x.Stdout + } + } + return nil } -func (m *ExecResponse) XXX_DiscardUnknown() { - xxx_messageInfo_ExecResponse.DiscardUnknown(m) + +func (x *ExecResponse) GetStderr() []byte { + if x != nil { + if x, ok := x.Response.(*ExecResponse_Stderr); ok { + return x.Stderr + } + } + return nil } -var xxx_messageInfo_ExecResponse proto.InternalMessageInfo +func (x *ExecResponse) GetExitCode() int32 { + if x != nil { + if x, ok := x.Response.(*ExecResponse_ExitCode); ok { + return x.ExitCode + } + } + return 0 +} type isExecResponse_Response interface { isExecResponse_Response() } type ExecResponse_Stdout struct { - Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3,oneof"` + Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3,oneof"` // Stdout data } type ExecResponse_Stderr struct { - Stderr []byte `protobuf:"bytes,2,opt,name=stderr,proto3,oneof"` + Stderr []byte `protobuf:"bytes,2,opt,name=stderr,proto3,oneof"` // Stderr data } type ExecResponse_ExitCode struct { - ExitCode int32 `protobuf:"varint,3,opt,name=exit_code,json=exitCode,proto3,oneof"` + ExitCode int32 `protobuf:"varint,3,opt,name=exit_code,json=exitCode,proto3,oneof"` // Command exit code (final message) } func (*ExecResponse_Stdout) isExecResponse_Response() {} @@ -234,891 +280,1007 @@ func (*ExecResponse_Stderr) isExecResponse_Response() {} func (*ExecResponse_ExitCode) isExecResponse_Response() {} -func (m *ExecResponse) GetResponse() isExecResponse_Response { - if m != nil { - return m.Response - } - return nil -} - -func (m *ExecResponse) GetStdout() []byte { - if x, ok := m.GetResponse().(*ExecResponse_Stdout); ok { - return x.Stdout - } - return nil -} - -func (m *ExecResponse) GetStderr() []byte { - if x, ok := m.GetResponse().(*ExecResponse_Stderr); ok { - return x.Stderr - } - return nil -} - -func (m *ExecResponse) GetExitCode() int32 { - if x, ok := m.GetResponse().(*ExecResponse_ExitCode); ok { - return x.ExitCode - } - return 0 -} - -// XXX_OneofWrappers is for the internal use of the proto package. -func (*ExecResponse) XXX_OneofWrappers() []interface{} { - return []interface{}{ - (*ExecResponse_Stdout)(nil), - (*ExecResponse_Stderr)(nil), - (*ExecResponse_ExitCode)(nil), - } -} - // CopyToGuestRequest represents messages for copying files to guest type CopyToGuestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Request: // // *CopyToGuestRequest_Start // *CopyToGuestRequest_Data // *CopyToGuestRequest_End - Request isCopyToGuestRequest_Request `protobuf_oneof:"request"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *CopyToGuestRequest) Reset() { *m = CopyToGuestRequest{} } -func (m *CopyToGuestRequest) String() string { return proto.CompactTextString(m) } -func (*CopyToGuestRequest) ProtoMessage() {} -func (*CopyToGuestRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{3} + Request isCopyToGuestRequest_Request `protobuf_oneof:"request"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *CopyToGuestRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyToGuestRequest.Unmarshal(m, b) -} -func (m *CopyToGuestRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyToGuestRequest.Marshal(b, m, deterministic) -} -func (m *CopyToGuestRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyToGuestRequest.Merge(m, src) -} -func (m *CopyToGuestRequest) XXX_Size() int { - return xxx_messageInfo_CopyToGuestRequest.Size(m) -} -func (m *CopyToGuestRequest) XXX_DiscardUnknown() { - xxx_messageInfo_CopyToGuestRequest.DiscardUnknown(m) +func (x *CopyToGuestRequest) Reset() { + *x = CopyToGuestRequest{} + mi := &file_lib_guest_guest_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -var xxx_messageInfo_CopyToGuestRequest proto.InternalMessageInfo - -type isCopyToGuestRequest_Request interface { - isCopyToGuestRequest_Request() +func (x *CopyToGuestRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -type CopyToGuestRequest_Start struct { - Start *CopyToGuestStart `protobuf:"bytes,1,opt,name=start,proto3,oneof"` -} +func (*CopyToGuestRequest) ProtoMessage() {} -type CopyToGuestRequest_Data struct { - Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +func (x *CopyToGuestRequest) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -type CopyToGuestRequest_End struct { - End *CopyToGuestEnd `protobuf:"bytes,3,opt,name=end,proto3,oneof"` +// Deprecated: Use CopyToGuestRequest.ProtoReflect.Descriptor instead. +func (*CopyToGuestRequest) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{3} } -func (*CopyToGuestRequest_Start) isCopyToGuestRequest_Request() {} - -func (*CopyToGuestRequest_Data) isCopyToGuestRequest_Request() {} - -func (*CopyToGuestRequest_End) isCopyToGuestRequest_Request() {} - -func (m *CopyToGuestRequest) GetRequest() isCopyToGuestRequest_Request { - if m != nil { - return m.Request +func (x *CopyToGuestRequest) GetRequest() isCopyToGuestRequest_Request { + if x != nil { + return x.Request } return nil } -func (m *CopyToGuestRequest) GetStart() *CopyToGuestStart { - if x, ok := m.GetRequest().(*CopyToGuestRequest_Start); ok { - return x.Start +func (x *CopyToGuestRequest) GetStart() *CopyToGuestStart { + if x != nil { + if x, ok := x.Request.(*CopyToGuestRequest_Start); ok { + return x.Start + } } return nil } -func (m *CopyToGuestRequest) GetData() []byte { - if x, ok := m.GetRequest().(*CopyToGuestRequest_Data); ok { - return x.Data +func (x *CopyToGuestRequest) GetData() []byte { + if x != nil { + if x, ok := x.Request.(*CopyToGuestRequest_Data); ok { + return x.Data + } } return nil } -func (m *CopyToGuestRequest) GetEnd() *CopyToGuestEnd { - if x, ok := m.GetRequest().(*CopyToGuestRequest_End); ok { - return x.End +func (x *CopyToGuestRequest) GetEnd() *CopyToGuestEnd { + if x != nil { + if x, ok := x.Request.(*CopyToGuestRequest_End); ok { + return x.End + } } return nil } -// XXX_OneofWrappers is for the internal use of the proto package. -func (*CopyToGuestRequest) XXX_OneofWrappers() []interface{} { - return []interface{}{ - (*CopyToGuestRequest_Start)(nil), - (*CopyToGuestRequest_Data)(nil), - (*CopyToGuestRequest_End)(nil), - } +type isCopyToGuestRequest_Request interface { + isCopyToGuestRequest_Request() } -// CopyToGuestStart initiates a copy-to-guest operation -type CopyToGuestStart struct { - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Mode uint32 `protobuf:"varint,2,opt,name=mode,proto3" json:"mode,omitempty"` - IsDir bool `protobuf:"varint,3,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` - Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` - Mtime int64 `protobuf:"varint,5,opt,name=mtime,proto3" json:"mtime,omitempty"` - Uid uint32 `protobuf:"varint,6,opt,name=uid,proto3" json:"uid,omitempty"` - Gid uint32 `protobuf:"varint,7,opt,name=gid,proto3" json:"gid,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *CopyToGuestStart) Reset() { *m = CopyToGuestStart{} } -func (m *CopyToGuestStart) String() string { return proto.CompactTextString(m) } -func (*CopyToGuestStart) ProtoMessage() {} -func (*CopyToGuestStart) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{4} +type CopyToGuestRequest_Start struct { + Start *CopyToGuestStart `protobuf:"bytes,1,opt,name=start,proto3,oneof"` // Initial copy request with metadata } -func (m *CopyToGuestStart) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyToGuestStart.Unmarshal(m, b) -} -func (m *CopyToGuestStart) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyToGuestStart.Marshal(b, m, deterministic) -} -func (m *CopyToGuestStart) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyToGuestStart.Merge(m, src) +type CopyToGuestRequest_Data struct { + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` // File content chunk } -func (m *CopyToGuestStart) XXX_Size() int { - return xxx_messageInfo_CopyToGuestStart.Size(m) + +type CopyToGuestRequest_End struct { + End *CopyToGuestEnd `protobuf:"bytes,3,opt,name=end,proto3,oneof"` // End of file marker } -func (m *CopyToGuestStart) XXX_DiscardUnknown() { - xxx_messageInfo_CopyToGuestStart.DiscardUnknown(m) + +func (*CopyToGuestRequest_Start) isCopyToGuestRequest_Request() {} + +func (*CopyToGuestRequest_Data) isCopyToGuestRequest_Request() {} + +func (*CopyToGuestRequest_End) isCopyToGuestRequest_Request() {} + +// CopyToGuestStart initiates a copy-to-guest operation +type CopyToGuestStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // Destination path in guest + Mode uint32 `protobuf:"varint,2,opt,name=mode,proto3" json:"mode,omitempty"` // File mode (permissions) + IsDir bool `protobuf:"varint,3,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` // True if this is a directory + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` // Expected total size (0 for directories) + Mtime int64 `protobuf:"varint,5,opt,name=mtime,proto3" json:"mtime,omitempty"` // Modification time (Unix timestamp) + Uid uint32 `protobuf:"varint,6,opt,name=uid,proto3" json:"uid,omitempty"` // User ID (archive mode only, 0 = use default) + Gid uint32 `protobuf:"varint,7,opt,name=gid,proto3" json:"gid,omitempty"` // Group ID (archive mode only, 0 = use default) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CopyToGuestStart) Reset() { + *x = CopyToGuestStart{} + mi := &file_lib_guest_guest_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CopyToGuestStart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CopyToGuestStart) ProtoMessage() {} + +func (x *CopyToGuestStart) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_CopyToGuestStart proto.InternalMessageInfo +// Deprecated: Use CopyToGuestStart.ProtoReflect.Descriptor instead. +func (*CopyToGuestStart) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{4} +} -func (m *CopyToGuestStart) GetPath() string { - if m != nil { - return m.Path +func (x *CopyToGuestStart) GetPath() string { + if x != nil { + return x.Path } return "" } -func (m *CopyToGuestStart) GetMode() uint32 { - if m != nil { - return m.Mode +func (x *CopyToGuestStart) GetMode() uint32 { + if x != nil { + return x.Mode } return 0 } -func (m *CopyToGuestStart) GetIsDir() bool { - if m != nil { - return m.IsDir +func (x *CopyToGuestStart) GetIsDir() bool { + if x != nil { + return x.IsDir } return false } -func (m *CopyToGuestStart) GetSize() int64 { - if m != nil { - return m.Size +func (x *CopyToGuestStart) GetSize() int64 { + if x != nil { + return x.Size } return 0 } -func (m *CopyToGuestStart) GetMtime() int64 { - if m != nil { - return m.Mtime +func (x *CopyToGuestStart) GetMtime() int64 { + if x != nil { + return x.Mtime } return 0 } -func (m *CopyToGuestStart) GetUid() uint32 { - if m != nil { - return m.Uid +func (x *CopyToGuestStart) GetUid() uint32 { + if x != nil { + return x.Uid } return 0 } -func (m *CopyToGuestStart) GetGid() uint32 { - if m != nil { - return m.Gid +func (x *CopyToGuestStart) GetGid() uint32 { + if x != nil { + return x.Gid } return 0 } // CopyToGuestEnd signals the end of a file transfer type CopyToGuestEnd struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *CopyToGuestEnd) Reset() { *m = CopyToGuestEnd{} } -func (m *CopyToGuestEnd) String() string { return proto.CompactTextString(m) } -func (*CopyToGuestEnd) ProtoMessage() {} -func (*CopyToGuestEnd) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{5} +func (x *CopyToGuestEnd) Reset() { + *x = CopyToGuestEnd{} + mi := &file_lib_guest_guest_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *CopyToGuestEnd) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyToGuestEnd.Unmarshal(m, b) -} -func (m *CopyToGuestEnd) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyToGuestEnd.Marshal(b, m, deterministic) -} -func (m *CopyToGuestEnd) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyToGuestEnd.Merge(m, src) -} -func (m *CopyToGuestEnd) XXX_Size() int { - return xxx_messageInfo_CopyToGuestEnd.Size(m) +func (x *CopyToGuestEnd) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *CopyToGuestEnd) XXX_DiscardUnknown() { - xxx_messageInfo_CopyToGuestEnd.DiscardUnknown(m) + +func (*CopyToGuestEnd) ProtoMessage() {} + +func (x *CopyToGuestEnd) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_CopyToGuestEnd proto.InternalMessageInfo +// Deprecated: Use CopyToGuestEnd.ProtoReflect.Descriptor instead. +func (*CopyToGuestEnd) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{5} +} // CopyToGuestResponse is the response after a copy-to-guest operation type CopyToGuestResponse struct { - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` - BytesWritten int64 `protobuf:"varint,3,opt,name=bytes_written,json=bytesWritten,proto3" json:"bytes_written,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` // Whether the copy succeeded + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Error message if failed + BytesWritten int64 `protobuf:"varint,3,opt,name=bytes_written,json=bytesWritten,proto3" json:"bytes_written,omitempty"` // Total bytes written + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *CopyToGuestResponse) Reset() { *m = CopyToGuestResponse{} } -func (m *CopyToGuestResponse) String() string { return proto.CompactTextString(m) } -func (*CopyToGuestResponse) ProtoMessage() {} -func (*CopyToGuestResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{6} +func (x *CopyToGuestResponse) Reset() { + *x = CopyToGuestResponse{} + mi := &file_lib_guest_guest_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *CopyToGuestResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyToGuestResponse.Unmarshal(m, b) -} -func (m *CopyToGuestResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyToGuestResponse.Marshal(b, m, deterministic) -} -func (m *CopyToGuestResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyToGuestResponse.Merge(m, src) -} -func (m *CopyToGuestResponse) XXX_Size() int { - return xxx_messageInfo_CopyToGuestResponse.Size(m) +func (x *CopyToGuestResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *CopyToGuestResponse) XXX_DiscardUnknown() { - xxx_messageInfo_CopyToGuestResponse.DiscardUnknown(m) + +func (*CopyToGuestResponse) ProtoMessage() {} + +func (x *CopyToGuestResponse) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_CopyToGuestResponse proto.InternalMessageInfo +// Deprecated: Use CopyToGuestResponse.ProtoReflect.Descriptor instead. +func (*CopyToGuestResponse) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{6} +} -func (m *CopyToGuestResponse) GetSuccess() bool { - if m != nil { - return m.Success +func (x *CopyToGuestResponse) GetSuccess() bool { + if x != nil { + return x.Success } return false } -func (m *CopyToGuestResponse) GetError() string { - if m != nil { - return m.Error +func (x *CopyToGuestResponse) GetError() string { + if x != nil { + return x.Error } return "" } -func (m *CopyToGuestResponse) GetBytesWritten() int64 { - if m != nil { - return m.BytesWritten +func (x *CopyToGuestResponse) GetBytesWritten() int64 { + if x != nil { + return x.BytesWritten } return 0 } // CopyFromGuestRequest initiates a copy-from-guest operation type CopyFromGuestRequest struct { - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - FollowLinks bool `protobuf:"varint,2,opt,name=follow_links,json=followLinks,proto3" json:"follow_links,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // Source path in guest + FollowLinks bool `protobuf:"varint,2,opt,name=follow_links,json=followLinks,proto3" json:"follow_links,omitempty"` // Follow symbolic links (like -L flag) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *CopyFromGuestRequest) Reset() { *m = CopyFromGuestRequest{} } -func (m *CopyFromGuestRequest) String() string { return proto.CompactTextString(m) } -func (*CopyFromGuestRequest) ProtoMessage() {} -func (*CopyFromGuestRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{7} +func (x *CopyFromGuestRequest) Reset() { + *x = CopyFromGuestRequest{} + mi := &file_lib_guest_guest_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *CopyFromGuestRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyFromGuestRequest.Unmarshal(m, b) -} -func (m *CopyFromGuestRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyFromGuestRequest.Marshal(b, m, deterministic) +func (x *CopyFromGuestRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *CopyFromGuestRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyFromGuestRequest.Merge(m, src) -} -func (m *CopyFromGuestRequest) XXX_Size() int { - return xxx_messageInfo_CopyFromGuestRequest.Size(m) -} -func (m *CopyFromGuestRequest) XXX_DiscardUnknown() { - xxx_messageInfo_CopyFromGuestRequest.DiscardUnknown(m) + +func (*CopyFromGuestRequest) ProtoMessage() {} + +func (x *CopyFromGuestRequest) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_CopyFromGuestRequest proto.InternalMessageInfo +// Deprecated: Use CopyFromGuestRequest.ProtoReflect.Descriptor instead. +func (*CopyFromGuestRequest) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{7} +} -func (m *CopyFromGuestRequest) GetPath() string { - if m != nil { - return m.Path +func (x *CopyFromGuestRequest) GetPath() string { + if x != nil { + return x.Path } return "" } -func (m *CopyFromGuestRequest) GetFollowLinks() bool { - if m != nil { - return m.FollowLinks +func (x *CopyFromGuestRequest) GetFollowLinks() bool { + if x != nil { + return x.FollowLinks } return false } // CopyFromGuestResponse streams file data from guest type CopyFromGuestResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Response: // // *CopyFromGuestResponse_Header // *CopyFromGuestResponse_Data // *CopyFromGuestResponse_End // *CopyFromGuestResponse_Error - Response isCopyFromGuestResponse_Response `protobuf_oneof:"response"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Response isCopyFromGuestResponse_Response `protobuf_oneof:"response"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *CopyFromGuestResponse) Reset() { *m = CopyFromGuestResponse{} } -func (m *CopyFromGuestResponse) String() string { return proto.CompactTextString(m) } -func (*CopyFromGuestResponse) ProtoMessage() {} -func (*CopyFromGuestResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{8} +func (x *CopyFromGuestResponse) Reset() { + *x = CopyFromGuestResponse{} + mi := &file_lib_guest_guest_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *CopyFromGuestResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyFromGuestResponse.Unmarshal(m, b) -} -func (m *CopyFromGuestResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyFromGuestResponse.Marshal(b, m, deterministic) -} -func (m *CopyFromGuestResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyFromGuestResponse.Merge(m, src) -} -func (m *CopyFromGuestResponse) XXX_Size() int { - return xxx_messageInfo_CopyFromGuestResponse.Size(m) -} -func (m *CopyFromGuestResponse) XXX_DiscardUnknown() { - xxx_messageInfo_CopyFromGuestResponse.DiscardUnknown(m) +func (x *CopyFromGuestResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -var xxx_messageInfo_CopyFromGuestResponse proto.InternalMessageInfo +func (*CopyFromGuestResponse) ProtoMessage() {} -type isCopyFromGuestResponse_Response interface { - isCopyFromGuestResponse_Response() -} - -type CopyFromGuestResponse_Header struct { - Header *CopyFromGuestHeader `protobuf:"bytes,1,opt,name=header,proto3,oneof"` -} - -type CopyFromGuestResponse_Data struct { - Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` -} - -type CopyFromGuestResponse_End struct { - End *CopyFromGuestEnd `protobuf:"bytes,3,opt,name=end,proto3,oneof"` +func (x *CopyFromGuestResponse) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_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) } -type CopyFromGuestResponse_Error struct { - Error *CopyFromGuestError `protobuf:"bytes,4,opt,name=error,proto3,oneof"` +// Deprecated: Use CopyFromGuestResponse.ProtoReflect.Descriptor instead. +func (*CopyFromGuestResponse) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{8} } -func (*CopyFromGuestResponse_Header) isCopyFromGuestResponse_Response() {} - -func (*CopyFromGuestResponse_Data) isCopyFromGuestResponse_Response() {} - -func (*CopyFromGuestResponse_End) isCopyFromGuestResponse_Response() {} - -func (*CopyFromGuestResponse_Error) isCopyFromGuestResponse_Response() {} - -func (m *CopyFromGuestResponse) GetResponse() isCopyFromGuestResponse_Response { - if m != nil { - return m.Response +func (x *CopyFromGuestResponse) GetResponse() isCopyFromGuestResponse_Response { + if x != nil { + return x.Response } return nil } -func (m *CopyFromGuestResponse) GetHeader() *CopyFromGuestHeader { - if x, ok := m.GetResponse().(*CopyFromGuestResponse_Header); ok { - return x.Header +func (x *CopyFromGuestResponse) GetHeader() *CopyFromGuestHeader { + if x != nil { + if x, ok := x.Response.(*CopyFromGuestResponse_Header); ok { + return x.Header + } } return nil } -func (m *CopyFromGuestResponse) GetData() []byte { - if x, ok := m.GetResponse().(*CopyFromGuestResponse_Data); ok { - return x.Data +func (x *CopyFromGuestResponse) GetData() []byte { + if x != nil { + if x, ok := x.Response.(*CopyFromGuestResponse_Data); ok { + return x.Data + } } return nil } -func (m *CopyFromGuestResponse) GetEnd() *CopyFromGuestEnd { - if x, ok := m.GetResponse().(*CopyFromGuestResponse_End); ok { - return x.End +func (x *CopyFromGuestResponse) GetEnd() *CopyFromGuestEnd { + if x != nil { + if x, ok := x.Response.(*CopyFromGuestResponse_End); ok { + return x.End + } } return nil } -func (m *CopyFromGuestResponse) GetError() *CopyFromGuestError { - if x, ok := m.GetResponse().(*CopyFromGuestResponse_Error); ok { - return x.Error +func (x *CopyFromGuestResponse) GetError() *CopyFromGuestError { + if x != nil { + if x, ok := x.Response.(*CopyFromGuestResponse_Error); ok { + return x.Error + } } return nil } -// XXX_OneofWrappers is for the internal use of the proto package. -func (*CopyFromGuestResponse) XXX_OneofWrappers() []interface{} { - return []interface{}{ - (*CopyFromGuestResponse_Header)(nil), - (*CopyFromGuestResponse_Data)(nil), - (*CopyFromGuestResponse_End)(nil), - (*CopyFromGuestResponse_Error)(nil), - } +type isCopyFromGuestResponse_Response interface { + isCopyFromGuestResponse_Response() } -// CopyFromGuestHeader provides metadata about a file being copied -type CopyFromGuestHeader struct { - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Mode uint32 `protobuf:"varint,2,opt,name=mode,proto3" json:"mode,omitempty"` - IsDir bool `protobuf:"varint,3,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` - IsSymlink bool `protobuf:"varint,4,opt,name=is_symlink,json=isSymlink,proto3" json:"is_symlink,omitempty"` - LinkTarget string `protobuf:"bytes,5,opt,name=link_target,json=linkTarget,proto3" json:"link_target,omitempty"` - Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"` - Mtime int64 `protobuf:"varint,7,opt,name=mtime,proto3" json:"mtime,omitempty"` - Uid uint32 `protobuf:"varint,8,opt,name=uid,proto3" json:"uid,omitempty"` - Gid uint32 `protobuf:"varint,9,opt,name=gid,proto3" json:"gid,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *CopyFromGuestHeader) Reset() { *m = CopyFromGuestHeader{} } -func (m *CopyFromGuestHeader) String() string { return proto.CompactTextString(m) } -func (*CopyFromGuestHeader) ProtoMessage() {} -func (*CopyFromGuestHeader) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{9} +type CopyFromGuestResponse_Header struct { + Header *CopyFromGuestHeader `protobuf:"bytes,1,opt,name=header,proto3,oneof"` // File/directory metadata } -func (m *CopyFromGuestHeader) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyFromGuestHeader.Unmarshal(m, b) -} -func (m *CopyFromGuestHeader) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyFromGuestHeader.Marshal(b, m, deterministic) +type CopyFromGuestResponse_Data struct { + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` // File content chunk } -func (m *CopyFromGuestHeader) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyFromGuestHeader.Merge(m, src) + +type CopyFromGuestResponse_End struct { + End *CopyFromGuestEnd `protobuf:"bytes,3,opt,name=end,proto3,oneof"` // End of file/transfer marker } -func (m *CopyFromGuestHeader) XXX_Size() int { - return xxx_messageInfo_CopyFromGuestHeader.Size(m) + +type CopyFromGuestResponse_Error struct { + Error *CopyFromGuestError `protobuf:"bytes,4,opt,name=error,proto3,oneof"` // Error during copy } -func (m *CopyFromGuestHeader) XXX_DiscardUnknown() { - xxx_messageInfo_CopyFromGuestHeader.DiscardUnknown(m) + +func (*CopyFromGuestResponse_Header) isCopyFromGuestResponse_Response() {} + +func (*CopyFromGuestResponse_Data) isCopyFromGuestResponse_Response() {} + +func (*CopyFromGuestResponse_End) isCopyFromGuestResponse_Response() {} + +func (*CopyFromGuestResponse_Error) isCopyFromGuestResponse_Response() {} + +// CopyFromGuestHeader provides metadata about a file being copied +type CopyFromGuestHeader struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // Relative path from copy root + Mode uint32 `protobuf:"varint,2,opt,name=mode,proto3" json:"mode,omitempty"` // File mode (permissions) + IsDir bool `protobuf:"varint,3,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` // True if this is a directory + IsSymlink bool `protobuf:"varint,4,opt,name=is_symlink,json=isSymlink,proto3" json:"is_symlink,omitempty"` // True if this is a symbolic link + LinkTarget string `protobuf:"bytes,5,opt,name=link_target,json=linkTarget,proto3" json:"link_target,omitempty"` // Symlink target (if is_symlink) + Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"` // File size (0 for directories) + Mtime int64 `protobuf:"varint,7,opt,name=mtime,proto3" json:"mtime,omitempty"` // Modification time (Unix timestamp) + Uid uint32 `protobuf:"varint,8,opt,name=uid,proto3" json:"uid,omitempty"` // User ID + Gid uint32 `protobuf:"varint,9,opt,name=gid,proto3" json:"gid,omitempty"` // Group ID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CopyFromGuestHeader) Reset() { + *x = CopyFromGuestHeader{} + mi := &file_lib_guest_guest_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CopyFromGuestHeader) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CopyFromGuestHeader) ProtoMessage() {} + +func (x *CopyFromGuestHeader) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_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) } -var xxx_messageInfo_CopyFromGuestHeader proto.InternalMessageInfo +// Deprecated: Use CopyFromGuestHeader.ProtoReflect.Descriptor instead. +func (*CopyFromGuestHeader) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{9} +} -func (m *CopyFromGuestHeader) GetPath() string { - if m != nil { - return m.Path +func (x *CopyFromGuestHeader) GetPath() string { + if x != nil { + return x.Path } return "" } -func (m *CopyFromGuestHeader) GetMode() uint32 { - if m != nil { - return m.Mode +func (x *CopyFromGuestHeader) GetMode() uint32 { + if x != nil { + return x.Mode } return 0 } -func (m *CopyFromGuestHeader) GetIsDir() bool { - if m != nil { - return m.IsDir +func (x *CopyFromGuestHeader) GetIsDir() bool { + if x != nil { + return x.IsDir } return false } -func (m *CopyFromGuestHeader) GetIsSymlink() bool { - if m != nil { - return m.IsSymlink +func (x *CopyFromGuestHeader) GetIsSymlink() bool { + if x != nil { + return x.IsSymlink } return false } -func (m *CopyFromGuestHeader) GetLinkTarget() string { - if m != nil { - return m.LinkTarget +func (x *CopyFromGuestHeader) GetLinkTarget() string { + if x != nil { + return x.LinkTarget } return "" } -func (m *CopyFromGuestHeader) GetSize() int64 { - if m != nil { - return m.Size +func (x *CopyFromGuestHeader) GetSize() int64 { + if x != nil { + return x.Size } return 0 } -func (m *CopyFromGuestHeader) GetMtime() int64 { - if m != nil { - return m.Mtime +func (x *CopyFromGuestHeader) GetMtime() int64 { + if x != nil { + return x.Mtime } return 0 } -func (m *CopyFromGuestHeader) GetUid() uint32 { - if m != nil { - return m.Uid +func (x *CopyFromGuestHeader) GetUid() uint32 { + if x != nil { + return x.Uid } return 0 } -func (m *CopyFromGuestHeader) GetGid() uint32 { - if m != nil { - return m.Gid +func (x *CopyFromGuestHeader) GetGid() uint32 { + if x != nil { + return x.Gid } return 0 } // CopyFromGuestEnd signals the end of a file or transfer type CopyFromGuestEnd struct { - Final bool `protobuf:"varint,1,opt,name=final,proto3" json:"final,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState `protogen:"open.v1"` + Final bool `protobuf:"varint,1,opt,name=final,proto3" json:"final,omitempty"` // True if this is the final file + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *CopyFromGuestEnd) Reset() { *m = CopyFromGuestEnd{} } -func (m *CopyFromGuestEnd) String() string { return proto.CompactTextString(m) } -func (*CopyFromGuestEnd) ProtoMessage() {} -func (*CopyFromGuestEnd) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{10} +func (x *CopyFromGuestEnd) Reset() { + *x = CopyFromGuestEnd{} + mi := &file_lib_guest_guest_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *CopyFromGuestEnd) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyFromGuestEnd.Unmarshal(m, b) +func (x *CopyFromGuestEnd) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *CopyFromGuestEnd) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyFromGuestEnd.Marshal(b, m, deterministic) -} -func (m *CopyFromGuestEnd) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyFromGuestEnd.Merge(m, src) -} -func (m *CopyFromGuestEnd) XXX_Size() int { - return xxx_messageInfo_CopyFromGuestEnd.Size(m) -} -func (m *CopyFromGuestEnd) XXX_DiscardUnknown() { - xxx_messageInfo_CopyFromGuestEnd.DiscardUnknown(m) + +func (*CopyFromGuestEnd) ProtoMessage() {} + +func (x *CopyFromGuestEnd) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_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) } -var xxx_messageInfo_CopyFromGuestEnd proto.InternalMessageInfo +// Deprecated: Use CopyFromGuestEnd.ProtoReflect.Descriptor instead. +func (*CopyFromGuestEnd) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{10} +} -func (m *CopyFromGuestEnd) GetFinal() bool { - if m != nil { - return m.Final +func (x *CopyFromGuestEnd) GetFinal() bool { + if x != nil { + return x.Final } return false } // CopyFromGuestError reports an error during copy type CopyFromGuestError struct { - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` // Error message + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` // Path that caused error (if applicable) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *CopyFromGuestError) Reset() { *m = CopyFromGuestError{} } -func (m *CopyFromGuestError) String() string { return proto.CompactTextString(m) } -func (*CopyFromGuestError) ProtoMessage() {} -func (*CopyFromGuestError) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{11} +func (x *CopyFromGuestError) Reset() { + *x = CopyFromGuestError{} + mi := &file_lib_guest_guest_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *CopyFromGuestError) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CopyFromGuestError.Unmarshal(m, b) +func (x *CopyFromGuestError) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *CopyFromGuestError) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CopyFromGuestError.Marshal(b, m, deterministic) -} -func (m *CopyFromGuestError) XXX_Merge(src proto.Message) { - xxx_messageInfo_CopyFromGuestError.Merge(m, src) -} -func (m *CopyFromGuestError) XXX_Size() int { - return xxx_messageInfo_CopyFromGuestError.Size(m) -} -func (m *CopyFromGuestError) XXX_DiscardUnknown() { - xxx_messageInfo_CopyFromGuestError.DiscardUnknown(m) + +func (*CopyFromGuestError) ProtoMessage() {} + +func (x *CopyFromGuestError) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_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) } -var xxx_messageInfo_CopyFromGuestError proto.InternalMessageInfo +// Deprecated: Use CopyFromGuestError.ProtoReflect.Descriptor instead. +func (*CopyFromGuestError) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{11} +} -func (m *CopyFromGuestError) GetMessage() string { - if m != nil { - return m.Message +func (x *CopyFromGuestError) GetMessage() string { + if x != nil { + return x.Message } return "" } -func (m *CopyFromGuestError) GetPath() string { - if m != nil { - return m.Path +func (x *CopyFromGuestError) GetPath() string { + if x != nil { + return x.Path } return "" } // StatPathRequest requests information about a path type StatPathRequest struct { - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - FollowLinks bool `protobuf:"varint,2,opt,name=follow_links,json=followLinks,proto3" json:"follow_links,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // Path to stat + FollowLinks bool `protobuf:"varint,2,opt,name=follow_links,json=followLinks,proto3" json:"follow_links,omitempty"` // Follow symbolic links + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *StatPathRequest) Reset() { *m = StatPathRequest{} } -func (m *StatPathRequest) String() string { return proto.CompactTextString(m) } -func (*StatPathRequest) ProtoMessage() {} -func (*StatPathRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{12} +func (x *StatPathRequest) Reset() { + *x = StatPathRequest{} + mi := &file_lib_guest_guest_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *StatPathRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_StatPathRequest.Unmarshal(m, b) -} -func (m *StatPathRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_StatPathRequest.Marshal(b, m, deterministic) -} -func (m *StatPathRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_StatPathRequest.Merge(m, src) -} -func (m *StatPathRequest) XXX_Size() int { - return xxx_messageInfo_StatPathRequest.Size(m) +func (x *StatPathRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (m *StatPathRequest) XXX_DiscardUnknown() { - xxx_messageInfo_StatPathRequest.DiscardUnknown(m) + +func (*StatPathRequest) ProtoMessage() {} + +func (x *StatPathRequest) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var xxx_messageInfo_StatPathRequest proto.InternalMessageInfo +// Deprecated: Use StatPathRequest.ProtoReflect.Descriptor instead. +func (*StatPathRequest) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{12} +} -func (m *StatPathRequest) GetPath() string { - if m != nil { - return m.Path +func (x *StatPathRequest) GetPath() string { + if x != nil { + return x.Path } return "" } -func (m *StatPathRequest) GetFollowLinks() bool { - if m != nil { - return m.FollowLinks +func (x *StatPathRequest) GetFollowLinks() bool { + if x != nil { + return x.FollowLinks } return false } // StatPathResponse contains information about a path type StatPathResponse struct { - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` - IsDir bool `protobuf:"varint,2,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` - IsFile bool `protobuf:"varint,3,opt,name=is_file,json=isFile,proto3" json:"is_file,omitempty"` - IsSymlink bool `protobuf:"varint,4,opt,name=is_symlink,json=isSymlink,proto3" json:"is_symlink,omitempty"` - LinkTarget string `protobuf:"bytes,5,opt,name=link_target,json=linkTarget,proto3" json:"link_target,omitempty"` - Mode uint32 `protobuf:"varint,6,opt,name=mode,proto3" json:"mode,omitempty"` - Size int64 `protobuf:"varint,7,opt,name=size,proto3" json:"size,omitempty"` - Error string `protobuf:"bytes,8,opt,name=error,proto3" json:"error,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *StatPathResponse) Reset() { *m = StatPathResponse{} } -func (m *StatPathResponse) String() string { return proto.CompactTextString(m) } -func (*StatPathResponse) ProtoMessage() {} -func (*StatPathResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_44c1cba55f3bcb29, []int{13} + state protoimpl.MessageState `protogen:"open.v1"` + Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` // Whether the path exists + IsDir bool `protobuf:"varint,2,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` // True if this is a directory + IsFile bool `protobuf:"varint,3,opt,name=is_file,json=isFile,proto3" json:"is_file,omitempty"` // True if this is a regular file + IsSymlink bool `protobuf:"varint,4,opt,name=is_symlink,json=isSymlink,proto3" json:"is_symlink,omitempty"` // True if this is a symbolic link (only if follow_links=false) + LinkTarget string `protobuf:"bytes,5,opt,name=link_target,json=linkTarget,proto3" json:"link_target,omitempty"` // Symlink target (if is_symlink) + Mode uint32 `protobuf:"varint,6,opt,name=mode,proto3" json:"mode,omitempty"` // File mode (permissions) + Size int64 `protobuf:"varint,7,opt,name=size,proto3" json:"size,omitempty"` // File size + Error string `protobuf:"bytes,8,opt,name=error,proto3" json:"error,omitempty"` // Error message if stat failed (e.g., permission denied) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatPathResponse) Reset() { + *x = StatPathResponse{} + mi := &file_lib_guest_guest_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatPathResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatPathResponse) ProtoMessage() {} + +func (x *StatPathResponse) ProtoReflect() protoreflect.Message { + mi := &file_lib_guest_guest_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -func (m *StatPathResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_StatPathResponse.Unmarshal(m, b) -} -func (m *StatPathResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_StatPathResponse.Marshal(b, m, deterministic) -} -func (m *StatPathResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_StatPathResponse.Merge(m, src) -} -func (m *StatPathResponse) XXX_Size() int { - return xxx_messageInfo_StatPathResponse.Size(m) -} -func (m *StatPathResponse) XXX_DiscardUnknown() { - xxx_messageInfo_StatPathResponse.DiscardUnknown(m) +// Deprecated: Use StatPathResponse.ProtoReflect.Descriptor instead. +func (*StatPathResponse) Descriptor() ([]byte, []int) { + return file_lib_guest_guest_proto_rawDescGZIP(), []int{13} } -var xxx_messageInfo_StatPathResponse proto.InternalMessageInfo - -func (m *StatPathResponse) GetExists() bool { - if m != nil { - return m.Exists +func (x *StatPathResponse) GetExists() bool { + if x != nil { + return x.Exists } return false } -func (m *StatPathResponse) GetIsDir() bool { - if m != nil { - return m.IsDir +func (x *StatPathResponse) GetIsDir() bool { + if x != nil { + return x.IsDir } return false } -func (m *StatPathResponse) GetIsFile() bool { - if m != nil { - return m.IsFile +func (x *StatPathResponse) GetIsFile() bool { + if x != nil { + return x.IsFile } return false } -func (m *StatPathResponse) GetIsSymlink() bool { - if m != nil { - return m.IsSymlink +func (x *StatPathResponse) GetIsSymlink() bool { + if x != nil { + return x.IsSymlink } return false } -func (m *StatPathResponse) GetLinkTarget() string { - if m != nil { - return m.LinkTarget +func (x *StatPathResponse) GetLinkTarget() string { + if x != nil { + return x.LinkTarget } return "" } -func (m *StatPathResponse) GetMode() uint32 { - if m != nil { - return m.Mode +func (x *StatPathResponse) GetMode() uint32 { + if x != nil { + return x.Mode } return 0 } -func (m *StatPathResponse) GetSize() int64 { - if m != nil { - return m.Size +func (x *StatPathResponse) GetSize() int64 { + if x != nil { + return x.Size } return 0 } -func (m *StatPathResponse) GetError() string { - if m != nil { - return m.Error +func (x *StatPathResponse) GetError() string { + if x != nil { + return x.Error } return "" } -func init() { - proto.RegisterType((*ExecRequest)(nil), "guest.ExecRequest") - proto.RegisterType((*ExecStart)(nil), "guest.ExecStart") - proto.RegisterMapType((map[string]string)(nil), "guest.ExecStart.EnvEntry") - proto.RegisterType((*ExecResponse)(nil), "guest.ExecResponse") - proto.RegisterType((*CopyToGuestRequest)(nil), "guest.CopyToGuestRequest") - proto.RegisterType((*CopyToGuestStart)(nil), "guest.CopyToGuestStart") - proto.RegisterType((*CopyToGuestEnd)(nil), "guest.CopyToGuestEnd") - proto.RegisterType((*CopyToGuestResponse)(nil), "guest.CopyToGuestResponse") - proto.RegisterType((*CopyFromGuestRequest)(nil), "guest.CopyFromGuestRequest") - proto.RegisterType((*CopyFromGuestResponse)(nil), "guest.CopyFromGuestResponse") - proto.RegisterType((*CopyFromGuestHeader)(nil), "guest.CopyFromGuestHeader") - proto.RegisterType((*CopyFromGuestEnd)(nil), "guest.CopyFromGuestEnd") - proto.RegisterType((*CopyFromGuestError)(nil), "guest.CopyFromGuestError") - proto.RegisterType((*StatPathRequest)(nil), "guest.StatPathRequest") - proto.RegisterType((*StatPathResponse)(nil), "guest.StatPathResponse") -} - -func init() { - proto.RegisterFile("lib/guest/guest.proto", fileDescriptor_44c1cba55f3bcb29) -} - -var fileDescriptor_44c1cba55f3bcb29 = []byte{ - // 897 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0x5d, 0x6f, 0xe3, 0x44, - 0x14, 0xad, 0xe3, 0xc4, 0xb1, 0x6f, 0xd2, 0xdd, 0x68, 0xb6, 0x1f, 0x6e, 0x60, 0x45, 0x30, 0x42, - 0x6b, 0xb4, 0x52, 0xb3, 0x74, 0x11, 0x42, 0xf0, 0xd6, 0xa5, 0x25, 0x0f, 0x8b, 0x84, 0xa6, 0x2b, - 0x21, 0xed, 0x4b, 0xe4, 0xda, 0xd3, 0x64, 0xa8, 0x3f, 0xc2, 0xcc, 0xa4, 0x6d, 0xf8, 0x17, 0xbc, - 0xf0, 0xca, 0x4f, 0xe2, 0x11, 0x9e, 0xf9, 0x25, 0xe8, 0xce, 0xd8, 0xa9, 0x9d, 0x86, 0xa7, 0xee, - 0x4b, 0x3b, 0xf7, 0xf8, 0xfa, 0xcc, 0xf5, 0x39, 0x67, 0x1c, 0xc3, 0x7e, 0xca, 0x2f, 0xc7, 0xb3, - 0x25, 0x93, 0xca, 0xfc, 0x3d, 0x5e, 0x88, 0x42, 0x15, 0xa4, 0xa3, 0x8b, 0xe0, 0x3d, 0xf4, 0xce, - 0xee, 0x58, 0x4c, 0xd9, 0xaf, 0x58, 0x92, 0x10, 0x3a, 0x52, 0x45, 0x42, 0xf9, 0xd6, 0xc8, 0x0a, - 0x7b, 0x27, 0x83, 0x63, 0x73, 0x0b, 0xb6, 0x5c, 0x20, 0x3e, 0xd9, 0xa1, 0xa6, 0x81, 0x1c, 0x60, - 0x67, 0xc2, 0x73, 0xbf, 0x35, 0xb2, 0xc2, 0xbe, 0xc1, 0x13, 0x9e, 0x9f, 0x7a, 0xd0, 0x15, 0x86, - 0x2c, 0xf8, 0xdb, 0x02, 0x6f, 0x7d, 0x27, 0xf1, 0xa1, 0x1b, 0x17, 0x59, 0x16, 0xe5, 0x89, 0x6f, - 0x8d, 0xec, 0xd0, 0xa3, 0x55, 0x49, 0x06, 0x60, 0x2b, 0xb5, 0xd2, 0x44, 0x2e, 0xc5, 0x25, 0x79, - 0x09, 0x36, 0xcb, 0x6f, 0x7c, 0x7b, 0x64, 0x87, 0xbd, 0x93, 0xa3, 0xcd, 0x21, 0x8e, 0xcf, 0xf2, - 0x9b, 0xb3, 0x5c, 0x89, 0x15, 0xc5, 0x2e, 0xbc, 0x3d, 0xbe, 0x4d, 0xfc, 0xf6, 0xc8, 0x0a, 0x3d, - 0x8a, 0x4b, 0xf2, 0x02, 0x9e, 0x2a, 0x9e, 0xb1, 0x62, 0xa9, 0xa6, 0x92, 0xc5, 0x45, 0x9e, 0x48, - 0xbf, 0x33, 0xb2, 0xc2, 0x0e, 0x7d, 0x52, 0xc2, 0x17, 0x06, 0x1d, 0x7e, 0x0d, 0x6e, 0xc5, 0x85, - 0x34, 0xd7, 0x6c, 0xa5, 0x1f, 0xdc, 0xa3, 0xb8, 0x24, 0x7b, 0xd0, 0xb9, 0x89, 0xd2, 0x25, 0xd3, - 0x93, 0x79, 0xd4, 0x14, 0xdf, 0xb6, 0xbe, 0xb1, 0x82, 0x0c, 0xfa, 0x46, 0x35, 0xb9, 0x28, 0x72, - 0xc9, 0x88, 0x0f, 0x8e, 0x54, 0x49, 0xb1, 0x34, 0xba, 0xa1, 0x1a, 0x65, 0x5d, 0x5e, 0x61, 0x42, - 0xac, 0x75, 0x2a, 0x6b, 0xf2, 0x1c, 0x3c, 0x76, 0xc7, 0xd5, 0x34, 0x2e, 0x12, 0xe6, 0xdb, 0x38, - 0xde, 0x64, 0x87, 0xba, 0x08, 0xbd, 0x29, 0x12, 0x76, 0x0a, 0xe0, 0x8a, 0x92, 0x3e, 0xf8, 0xdd, - 0x02, 0xf2, 0xa6, 0x58, 0xac, 0xde, 0x15, 0x3f, 0xa0, 0x12, 0x95, 0x59, 0xe3, 0xa6, 0x59, 0x87, - 0xa5, 0x4e, 0xb5, 0xce, 0x0d, 0xcf, 0xf6, 0xa0, 0x9d, 0x44, 0x2a, 0x5a, 0x8f, 0xa2, 0x2b, 0xf2, - 0x05, 0x8a, 0x9d, 0xe8, 0x11, 0x7a, 0x27, 0xfb, 0x0f, 0x49, 0xce, 0xf2, 0x64, 0xb2, 0x83, 0x52, - 0x27, 0x75, 0x73, 0xff, 0xb4, 0x60, 0xb0, 0xb9, 0x13, 0x21, 0xd0, 0x5e, 0x44, 0x6a, 0x5e, 0x8a, - 0xa8, 0xd7, 0x88, 0x65, 0xf8, 0x88, 0xb8, 0xe9, 0x2e, 0xd5, 0x6b, 0xb2, 0x0f, 0x0e, 0x97, 0xd3, - 0x84, 0x0b, 0xbd, 0xab, 0x4b, 0x3b, 0x5c, 0x7e, 0xcf, 0x05, 0xb6, 0x4a, 0xfe, 0x1b, 0xd3, 0x56, - 0xda, 0x54, 0xaf, 0xd1, 0x84, 0x0c, 0x5d, 0xd3, 0x0e, 0xda, 0xd4, 0x14, 0x68, 0xd6, 0x92, 0x27, - 0xbe, 0xa3, 0x39, 0x71, 0x89, 0xc8, 0x8c, 0x27, 0x7e, 0xd7, 0x20, 0x33, 0x9e, 0x04, 0x03, 0x78, - 0xd2, 0x7c, 0x8a, 0xe0, 0x17, 0x78, 0xd6, 0x90, 0x71, 0xed, 0x5e, 0x57, 0x2e, 0xe3, 0x98, 0x49, - 0xa9, 0x07, 0x77, 0x69, 0x55, 0xe2, 0xe6, 0x4c, 0x88, 0x42, 0x54, 0x09, 0xd0, 0x05, 0xf9, 0x0c, - 0x76, 0x2f, 0x57, 0x8a, 0xc9, 0xe9, 0xad, 0xe0, 0x4a, 0xb1, 0x5c, 0x3f, 0x84, 0x4d, 0xfb, 0x1a, - 0xfc, 0xd9, 0x60, 0xc1, 0x8f, 0xb0, 0x87, 0x7b, 0x9d, 0x8b, 0x22, 0x6b, 0x98, 0xb6, 0x4d, 0xa2, - 0x4f, 0xa1, 0x7f, 0x55, 0xa4, 0x69, 0x71, 0x3b, 0x4d, 0x79, 0x7e, 0x2d, 0xcb, 0x93, 0xd0, 0x33, - 0xd8, 0x5b, 0x84, 0x82, 0xbf, 0x2c, 0xd8, 0xdf, 0xe0, 0x2b, 0xa7, 0xff, 0x0a, 0x9c, 0x39, 0x8b, - 0x12, 0x26, 0xca, 0x18, 0x0c, 0x6b, 0x0e, 0xae, 0xbb, 0x27, 0xba, 0x03, 0xd3, 0x67, 0x7a, 0xff, - 0x27, 0x0a, 0x2f, 0xeb, 0x51, 0x38, 0xdc, 0x46, 0x74, 0x1f, 0x06, 0xf2, 0x65, 0x25, 0x4e, 0x5b, - 0xb7, 0x1f, 0x6d, 0x6d, 0xc7, 0x06, 0x0c, 0xa0, 0xee, 0x6c, 0x84, 0xfa, 0x5f, 0xcb, 0xb8, 0xb1, - 0x31, 0xe3, 0x63, 0x33, 0xf4, 0x1c, 0x80, 0xcb, 0xa9, 0x5c, 0x65, 0x28, 0xa5, 0x1e, 0xcd, 0xa5, - 0x1e, 0x97, 0x17, 0x06, 0x20, 0x9f, 0x40, 0x0f, 0xff, 0x4f, 0x55, 0x24, 0x66, 0x4c, 0xe9, 0x50, - 0x79, 0x14, 0x10, 0x7a, 0xa7, 0x91, 0x75, 0x06, 0x9d, 0x6d, 0x19, 0xec, 0x6e, 0xc9, 0xa0, 0xfb, - 0x20, 0x83, 0xde, 0x7d, 0x06, 0x43, 0x73, 0x48, 0xea, 0xf2, 0x21, 0xdb, 0x15, 0xcf, 0xa3, 0xb4, - 0x0c, 0x9b, 0x29, 0x82, 0x53, 0x73, 0xc4, 0x9b, 0xca, 0x61, 0x34, 0x33, 0x26, 0x65, 0x34, 0x63, - 0xa5, 0x1e, 0x55, 0xb9, 0x96, 0xa9, 0x75, 0x2f, 0x53, 0x30, 0x81, 0xa7, 0x17, 0x2a, 0x52, 0x3f, - 0x45, 0x6a, 0xfe, 0xc8, 0xb8, 0xfd, 0x63, 0xc1, 0xe0, 0x9e, 0xaa, 0x4c, 0xda, 0x01, 0x38, 0xec, - 0x8e, 0x4b, 0x55, 0x1d, 0x93, 0xb2, 0xaa, 0x39, 0xd1, 0xaa, 0x3b, 0x71, 0x08, 0x5d, 0x2e, 0xa7, - 0x57, 0x3c, 0x65, 0xa5, 0x43, 0x0e, 0x97, 0xe7, 0x3c, 0x65, 0x1f, 0xc2, 0x22, 0x9d, 0x06, 0xa7, - 0x96, 0x86, 0xca, 0xb6, 0x6e, 0xd3, 0x36, 0x13, 0x50, 0xb7, 0x76, 0x7a, 0x4f, 0xfe, 0x68, 0x41, - 0xdf, 0xbc, 0xb2, 0x98, 0xb8, 0xe1, 0x31, 0x23, 0xaf, 0xa1, 0x8d, 0x2f, 0x73, 0x42, 0x6a, 0xbf, - 0x33, 0xa5, 0x7c, 0xc3, 0x67, 0x0d, 0xcc, 0xe8, 0x10, 0x5a, 0xaf, 0x2c, 0x72, 0x0e, 0xbd, 0xda, - 0xab, 0x84, 0x1c, 0x3d, 0x7c, 0x6d, 0x56, 0x14, 0xc3, 0x6d, 0x97, 0x2a, 0x26, 0xf2, 0x16, 0x76, - 0x1b, 0xb6, 0x93, 0x8f, 0xb6, 0x1d, 0xa3, 0x8a, 0xeb, 0xe3, 0xed, 0x17, 0x0d, 0xdb, 0x2b, 0x8b, - 0x7c, 0x07, 0x6e, 0xe5, 0x1a, 0x39, 0x28, 0x7b, 0x37, 0x12, 0x31, 0x3c, 0x7c, 0x80, 0x9b, 0xdb, - 0x4f, 0x5f, 0xbc, 0xff, 0x7c, 0xc6, 0xd5, 0x7c, 0x79, 0x79, 0x1c, 0x17, 0xd9, 0xb8, 0xc8, 0xaf, - 0x99, 0xc8, 0x59, 0x3a, 0x9e, 0xaf, 0x16, 0x2c, 0x8b, 0xf2, 0xf1, 0xfa, 0x33, 0xe2, 0xd2, 0xd1, - 0x5f, 0x10, 0xaf, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x85, 0xa3, 0x7e, 0x65, 0x5a, 0x08, 0x00, - 0x00, +var File_lib_guest_guest_proto protoreflect.FileDescriptor + +const file_lib_guest_guest_proto_rawDesc = "" + + "\n" + + "\x15lib/guest/guest.proto\x12\x05guest\"Z\n" + + "\vExecRequest\x12(\n" + + "\x05start\x18\x01 \x01(\v2\x10.guest.ExecStartH\x00R\x05start\x12\x16\n" + + "\x05stdin\x18\x02 \x01(\fH\x00R\x05stdinB\t\n" + + "\arequest\"\xd7\x01\n" + + "\tExecStart\x12\x18\n" + + "\acommand\x18\x01 \x03(\tR\acommand\x12\x10\n" + + "\x03tty\x18\x02 \x01(\bR\x03tty\x12+\n" + + "\x03env\x18\x03 \x03(\v2\x19.guest.ExecStart.EnvEntryR\x03env\x12\x10\n" + + "\x03cwd\x18\x04 \x01(\tR\x03cwd\x12'\n" + + "\x0ftimeout_seconds\x18\x05 \x01(\x05R\x0etimeoutSeconds\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"m\n" + + "\fExecResponse\x12\x18\n" + + "\x06stdout\x18\x01 \x01(\fH\x00R\x06stdout\x12\x18\n" + + "\x06stderr\x18\x02 \x01(\fH\x00R\x06stderr\x12\x1d\n" + + "\texit_code\x18\x03 \x01(\x05H\x00R\bexitCodeB\n" + + "\n" + + "\bresponse\"\x91\x01\n" + + "\x12CopyToGuestRequest\x12/\n" + + "\x05start\x18\x01 \x01(\v2\x17.guest.CopyToGuestStartH\x00R\x05start\x12\x14\n" + + "\x04data\x18\x02 \x01(\fH\x00R\x04data\x12)\n" + + "\x03end\x18\x03 \x01(\v2\x15.guest.CopyToGuestEndH\x00R\x03endB\t\n" + + "\arequest\"\x9f\x01\n" + + "\x10CopyToGuestStart\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + + "\x04mode\x18\x02 \x01(\rR\x04mode\x12\x15\n" + + "\x06is_dir\x18\x03 \x01(\bR\x05isDir\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\x12\x14\n" + + "\x05mtime\x18\x05 \x01(\x03R\x05mtime\x12\x10\n" + + "\x03uid\x18\x06 \x01(\rR\x03uid\x12\x10\n" + + "\x03gid\x18\a \x01(\rR\x03gid\"\x10\n" + + "\x0eCopyToGuestEnd\"j\n" + + "\x13CopyToGuestResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\x12#\n" + + "\rbytes_written\x18\x03 \x01(\x03R\fbytesWritten\"M\n" + + "\x14CopyFromGuestRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12!\n" + + "\ffollow_links\x18\x02 \x01(\bR\vfollowLinks\"\xcf\x01\n" + + "\x15CopyFromGuestResponse\x124\n" + + "\x06header\x18\x01 \x01(\v2\x1a.guest.CopyFromGuestHeaderH\x00R\x06header\x12\x14\n" + + "\x04data\x18\x02 \x01(\fH\x00R\x04data\x12+\n" + + "\x03end\x18\x03 \x01(\v2\x17.guest.CopyFromGuestEndH\x00R\x03end\x121\n" + + "\x05error\x18\x04 \x01(\v2\x19.guest.CopyFromGuestErrorH\x00R\x05errorB\n" + + "\n" + + "\bresponse\"\xe2\x01\n" + + "\x13CopyFromGuestHeader\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + + "\x04mode\x18\x02 \x01(\rR\x04mode\x12\x15\n" + + "\x06is_dir\x18\x03 \x01(\bR\x05isDir\x12\x1d\n" + + "\n" + + "is_symlink\x18\x04 \x01(\bR\tisSymlink\x12\x1f\n" + + "\vlink_target\x18\x05 \x01(\tR\n" + + "linkTarget\x12\x12\n" + + "\x04size\x18\x06 \x01(\x03R\x04size\x12\x14\n" + + "\x05mtime\x18\a \x01(\x03R\x05mtime\x12\x10\n" + + "\x03uid\x18\b \x01(\rR\x03uid\x12\x10\n" + + "\x03gid\x18\t \x01(\rR\x03gid\"(\n" + + "\x10CopyFromGuestEnd\x12\x14\n" + + "\x05final\x18\x01 \x01(\bR\x05final\"B\n" + + "\x12CopyFromGuestError\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\"H\n" + + "\x0fStatPathRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12!\n" + + "\ffollow_links\x18\x02 \x01(\bR\vfollowLinks\"\xd8\x01\n" + + "\x10StatPathResponse\x12\x16\n" + + "\x06exists\x18\x01 \x01(\bR\x06exists\x12\x15\n" + + "\x06is_dir\x18\x02 \x01(\bR\x05isDir\x12\x17\n" + + "\ais_file\x18\x03 \x01(\bR\x06isFile\x12\x1d\n" + + "\n" + + "is_symlink\x18\x04 \x01(\bR\tisSymlink\x12\x1f\n" + + "\vlink_target\x18\x05 \x01(\tR\n" + + "linkTarget\x12\x12\n" + + "\x04mode\x18\x06 \x01(\rR\x04mode\x12\x12\n" + + "\x04size\x18\a \x01(\x03R\x04size\x12\x14\n" + + "\x05error\x18\b \x01(\tR\x05error2\x96\x02\n" + + "\fGuestService\x123\n" + + "\x04Exec\x12\x12.guest.ExecRequest\x1a\x13.guest.ExecResponse(\x010\x01\x12F\n" + + "\vCopyToGuest\x12\x19.guest.CopyToGuestRequest\x1a\x1a.guest.CopyToGuestResponse(\x01\x12L\n" + + "\rCopyFromGuest\x12\x1b.guest.CopyFromGuestRequest\x1a\x1c.guest.CopyFromGuestResponse0\x01\x12;\n" + + "\bStatPath\x12\x16.guest.StatPathRequest\x1a\x17.guest.StatPathResponseB'Z%github.com/onkernel/hypeman/lib/guestb\x06proto3" + +var ( + file_lib_guest_guest_proto_rawDescOnce sync.Once + file_lib_guest_guest_proto_rawDescData []byte +) + +func file_lib_guest_guest_proto_rawDescGZIP() []byte { + file_lib_guest_guest_proto_rawDescOnce.Do(func() { + file_lib_guest_guest_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_lib_guest_guest_proto_rawDesc), len(file_lib_guest_guest_proto_rawDesc))) + }) + return file_lib_guest_guest_proto_rawDescData +} + +var file_lib_guest_guest_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_lib_guest_guest_proto_goTypes = []any{ + (*ExecRequest)(nil), // 0: guest.ExecRequest + (*ExecStart)(nil), // 1: guest.ExecStart + (*ExecResponse)(nil), // 2: guest.ExecResponse + (*CopyToGuestRequest)(nil), // 3: guest.CopyToGuestRequest + (*CopyToGuestStart)(nil), // 4: guest.CopyToGuestStart + (*CopyToGuestEnd)(nil), // 5: guest.CopyToGuestEnd + (*CopyToGuestResponse)(nil), // 6: guest.CopyToGuestResponse + (*CopyFromGuestRequest)(nil), // 7: guest.CopyFromGuestRequest + (*CopyFromGuestResponse)(nil), // 8: guest.CopyFromGuestResponse + (*CopyFromGuestHeader)(nil), // 9: guest.CopyFromGuestHeader + (*CopyFromGuestEnd)(nil), // 10: guest.CopyFromGuestEnd + (*CopyFromGuestError)(nil), // 11: guest.CopyFromGuestError + (*StatPathRequest)(nil), // 12: guest.StatPathRequest + (*StatPathResponse)(nil), // 13: guest.StatPathResponse + nil, // 14: guest.ExecStart.EnvEntry +} +var file_lib_guest_guest_proto_depIdxs = []int32{ + 1, // 0: guest.ExecRequest.start:type_name -> guest.ExecStart + 14, // 1: guest.ExecStart.env:type_name -> guest.ExecStart.EnvEntry + 4, // 2: guest.CopyToGuestRequest.start:type_name -> guest.CopyToGuestStart + 5, // 3: guest.CopyToGuestRequest.end:type_name -> guest.CopyToGuestEnd + 9, // 4: guest.CopyFromGuestResponse.header:type_name -> guest.CopyFromGuestHeader + 10, // 5: guest.CopyFromGuestResponse.end:type_name -> guest.CopyFromGuestEnd + 11, // 6: guest.CopyFromGuestResponse.error:type_name -> guest.CopyFromGuestError + 0, // 7: guest.GuestService.Exec:input_type -> guest.ExecRequest + 3, // 8: guest.GuestService.CopyToGuest:input_type -> guest.CopyToGuestRequest + 7, // 9: guest.GuestService.CopyFromGuest:input_type -> guest.CopyFromGuestRequest + 12, // 10: guest.GuestService.StatPath:input_type -> guest.StatPathRequest + 2, // 11: guest.GuestService.Exec:output_type -> guest.ExecResponse + 6, // 12: guest.GuestService.CopyToGuest:output_type -> guest.CopyToGuestResponse + 8, // 13: guest.GuestService.CopyFromGuest:output_type -> guest.CopyFromGuestResponse + 13, // 14: guest.GuestService.StatPath:output_type -> guest.StatPathResponse + 11, // [11:15] is the sub-list for method output_type + 7, // [7:11] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_lib_guest_guest_proto_init() } +func file_lib_guest_guest_proto_init() { + if File_lib_guest_guest_proto != nil { + return + } + file_lib_guest_guest_proto_msgTypes[0].OneofWrappers = []any{ + (*ExecRequest_Start)(nil), + (*ExecRequest_Stdin)(nil), + } + file_lib_guest_guest_proto_msgTypes[2].OneofWrappers = []any{ + (*ExecResponse_Stdout)(nil), + (*ExecResponse_Stderr)(nil), + (*ExecResponse_ExitCode)(nil), + } + file_lib_guest_guest_proto_msgTypes[3].OneofWrappers = []any{ + (*CopyToGuestRequest_Start)(nil), + (*CopyToGuestRequest_Data)(nil), + (*CopyToGuestRequest_End)(nil), + } + file_lib_guest_guest_proto_msgTypes[8].OneofWrappers = []any{ + (*CopyFromGuestResponse_Header)(nil), + (*CopyFromGuestResponse_Data)(nil), + (*CopyFromGuestResponse_End)(nil), + (*CopyFromGuestResponse_Error)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_lib_guest_guest_proto_rawDesc), len(file_lib_guest_guest_proto_rawDesc)), + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_lib_guest_guest_proto_goTypes, + DependencyIndexes: file_lib_guest_guest_proto_depIdxs, + MessageInfos: file_lib_guest_guest_proto_msgTypes, + }.Build() + File_lib_guest_guest_proto = out.File + file_lib_guest_guest_proto_goTypes = nil + file_lib_guest_guest_proto_depIdxs = nil } diff --git a/lib/guest/guest_drpc.pb.go b/lib/guest/guest_drpc.pb.go new file mode 100644 index 00000000..2fd2d519 --- /dev/null +++ b/lib/guest/guest_drpc.pb.go @@ -0,0 +1,346 @@ +// Code generated by protoc-gen-go-drpc. DO NOT EDIT. +// protoc-gen-go-drpc version: v0.0.34 +// source: lib/guest/guest.proto + +package guest + +import ( + context "context" + errors "errors" + protojson "google.golang.org/protobuf/encoding/protojson" + proto "google.golang.org/protobuf/proto" + drpc "storj.io/drpc" + drpcerr "storj.io/drpc/drpcerr" +) + +type drpcEncoding_File_lib_guest_guest_proto struct{} + +func (drpcEncoding_File_lib_guest_guest_proto) Marshal(msg drpc.Message) ([]byte, error) { + return proto.Marshal(msg.(proto.Message)) +} + +func (drpcEncoding_File_lib_guest_guest_proto) MarshalAppend(buf []byte, msg drpc.Message) ([]byte, error) { + return proto.MarshalOptions{}.MarshalAppend(buf, msg.(proto.Message)) +} + +func (drpcEncoding_File_lib_guest_guest_proto) Unmarshal(buf []byte, msg drpc.Message) error { + return proto.Unmarshal(buf, msg.(proto.Message)) +} + +func (drpcEncoding_File_lib_guest_guest_proto) JSONMarshal(msg drpc.Message) ([]byte, error) { + return protojson.Marshal(msg.(proto.Message)) +} + +func (drpcEncoding_File_lib_guest_guest_proto) JSONUnmarshal(buf []byte, msg drpc.Message) error { + return protojson.Unmarshal(buf, msg.(proto.Message)) +} + +type DRPCGuestServiceClient interface { + DRPCConn() drpc.Conn + + Exec(ctx context.Context) (DRPCGuestService_ExecClient, error) + CopyToGuest(ctx context.Context) (DRPCGuestService_CopyToGuestClient, error) + CopyFromGuest(ctx context.Context, in *CopyFromGuestRequest) (DRPCGuestService_CopyFromGuestClient, error) + StatPath(ctx context.Context, in *StatPathRequest) (*StatPathResponse, error) +} + +type drpcGuestServiceClient struct { + cc drpc.Conn +} + +func NewDRPCGuestServiceClient(cc drpc.Conn) DRPCGuestServiceClient { + return &drpcGuestServiceClient{cc} +} + +func (c *drpcGuestServiceClient) DRPCConn() drpc.Conn { return c.cc } + +func (c *drpcGuestServiceClient) Exec(ctx context.Context) (DRPCGuestService_ExecClient, error) { + stream, err := c.cc.NewStream(ctx, "/guest.GuestService/Exec", drpcEncoding_File_lib_guest_guest_proto{}) + if err != nil { + return nil, err + } + x := &drpcGuestService_ExecClient{stream} + return x, nil +} + +type DRPCGuestService_ExecClient interface { + drpc.Stream + Send(*ExecRequest) error + Recv() (*ExecResponse, error) +} + +type drpcGuestService_ExecClient struct { + drpc.Stream +} + +func (x *drpcGuestService_ExecClient) GetStream() drpc.Stream { + return x.Stream +} + +func (x *drpcGuestService_ExecClient) Send(m *ExecRequest) error { + return x.MsgSend(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +func (x *drpcGuestService_ExecClient) Recv() (*ExecResponse, error) { + m := new(ExecResponse) + if err := x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcGuestService_ExecClient) RecvMsg(m *ExecResponse) error { + return x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +func (c *drpcGuestServiceClient) CopyToGuest(ctx context.Context) (DRPCGuestService_CopyToGuestClient, error) { + stream, err := c.cc.NewStream(ctx, "/guest.GuestService/CopyToGuest", drpcEncoding_File_lib_guest_guest_proto{}) + if err != nil { + return nil, err + } + x := &drpcGuestService_CopyToGuestClient{stream} + return x, nil +} + +type DRPCGuestService_CopyToGuestClient interface { + drpc.Stream + Send(*CopyToGuestRequest) error + CloseAndRecv() (*CopyToGuestResponse, error) +} + +type drpcGuestService_CopyToGuestClient struct { + drpc.Stream +} + +func (x *drpcGuestService_CopyToGuestClient) GetStream() drpc.Stream { + return x.Stream +} + +func (x *drpcGuestService_CopyToGuestClient) Send(m *CopyToGuestRequest) error { + return x.MsgSend(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +func (x *drpcGuestService_CopyToGuestClient) CloseAndRecv() (*CopyToGuestResponse, error) { + if err := x.CloseSend(); err != nil { + return nil, err + } + m := new(CopyToGuestResponse) + if err := x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcGuestService_CopyToGuestClient) CloseAndRecvMsg(m *CopyToGuestResponse) error { + if err := x.CloseSend(); err != nil { + return err + } + return x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +func (c *drpcGuestServiceClient) CopyFromGuest(ctx context.Context, in *CopyFromGuestRequest) (DRPCGuestService_CopyFromGuestClient, error) { + stream, err := c.cc.NewStream(ctx, "/guest.GuestService/CopyFromGuest", drpcEncoding_File_lib_guest_guest_proto{}) + if err != nil { + return nil, err + } + x := &drpcGuestService_CopyFromGuestClient{stream} + if err := x.MsgSend(in, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return nil, err + } + if err := x.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type DRPCGuestService_CopyFromGuestClient interface { + drpc.Stream + Recv() (*CopyFromGuestResponse, error) +} + +type drpcGuestService_CopyFromGuestClient struct { + drpc.Stream +} + +func (x *drpcGuestService_CopyFromGuestClient) GetStream() drpc.Stream { + return x.Stream +} + +func (x *drpcGuestService_CopyFromGuestClient) Recv() (*CopyFromGuestResponse, error) { + m := new(CopyFromGuestResponse) + if err := x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcGuestService_CopyFromGuestClient) RecvMsg(m *CopyFromGuestResponse) error { + return x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +func (c *drpcGuestServiceClient) StatPath(ctx context.Context, in *StatPathRequest) (*StatPathResponse, error) { + out := new(StatPathResponse) + err := c.cc.Invoke(ctx, "/guest.GuestService/StatPath", drpcEncoding_File_lib_guest_guest_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + +type DRPCGuestServiceServer interface { + Exec(DRPCGuestService_ExecStream) error + CopyToGuest(DRPCGuestService_CopyToGuestStream) error + CopyFromGuest(*CopyFromGuestRequest, DRPCGuestService_CopyFromGuestStream) error + StatPath(context.Context, *StatPathRequest) (*StatPathResponse, error) +} + +type DRPCGuestServiceUnimplementedServer struct{} + +func (s *DRPCGuestServiceUnimplementedServer) Exec(DRPCGuestService_ExecStream) error { + return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +func (s *DRPCGuestServiceUnimplementedServer) CopyToGuest(DRPCGuestService_CopyToGuestStream) error { + return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +func (s *DRPCGuestServiceUnimplementedServer) CopyFromGuest(*CopyFromGuestRequest, DRPCGuestService_CopyFromGuestStream) error { + return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +func (s *DRPCGuestServiceUnimplementedServer) StatPath(context.Context, *StatPathRequest) (*StatPathResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + +type DRPCGuestServiceDescription struct{} + +func (DRPCGuestServiceDescription) NumMethods() int { return 4 } + +func (DRPCGuestServiceDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { + switch n { + case 0: + return "/guest.GuestService/Exec", drpcEncoding_File_lib_guest_guest_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return nil, srv.(DRPCGuestServiceServer). + Exec( + &drpcGuestService_ExecStream{in1.(drpc.Stream)}, + ) + }, DRPCGuestServiceServer.Exec, true + case 1: + return "/guest.GuestService/CopyToGuest", drpcEncoding_File_lib_guest_guest_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return nil, srv.(DRPCGuestServiceServer). + CopyToGuest( + &drpcGuestService_CopyToGuestStream{in1.(drpc.Stream)}, + ) + }, DRPCGuestServiceServer.CopyToGuest, true + case 2: + return "/guest.GuestService/CopyFromGuest", drpcEncoding_File_lib_guest_guest_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return nil, srv.(DRPCGuestServiceServer). + CopyFromGuest( + in1.(*CopyFromGuestRequest), + &drpcGuestService_CopyFromGuestStream{in2.(drpc.Stream)}, + ) + }, DRPCGuestServiceServer.CopyFromGuest, true + case 3: + return "/guest.GuestService/StatPath", drpcEncoding_File_lib_guest_guest_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCGuestServiceServer). + StatPath( + ctx, + in1.(*StatPathRequest), + ) + }, DRPCGuestServiceServer.StatPath, true + default: + return "", nil, nil, nil, false + } +} + +func DRPCRegisterGuestService(mux drpc.Mux, impl DRPCGuestServiceServer) error { + return mux.Register(impl, DRPCGuestServiceDescription{}) +} + +type DRPCGuestService_ExecStream interface { + drpc.Stream + Send(*ExecResponse) error + Recv() (*ExecRequest, error) +} + +type drpcGuestService_ExecStream struct { + drpc.Stream +} + +func (x *drpcGuestService_ExecStream) Send(m *ExecResponse) error { + return x.MsgSend(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +func (x *drpcGuestService_ExecStream) Recv() (*ExecRequest, error) { + m := new(ExecRequest) + if err := x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcGuestService_ExecStream) RecvMsg(m *ExecRequest) error { + return x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +type DRPCGuestService_CopyToGuestStream interface { + drpc.Stream + SendAndClose(*CopyToGuestResponse) error + Recv() (*CopyToGuestRequest, error) +} + +type drpcGuestService_CopyToGuestStream struct { + drpc.Stream +} + +func (x *drpcGuestService_CopyToGuestStream) SendAndClose(m *CopyToGuestResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return err + } + return x.CloseSend() +} + +func (x *drpcGuestService_CopyToGuestStream) Recv() (*CopyToGuestRequest, error) { + m := new(CopyToGuestRequest) + if err := x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return nil, err + } + return m, nil +} + +func (x *drpcGuestService_CopyToGuestStream) RecvMsg(m *CopyToGuestRequest) error { + return x.MsgRecv(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +type DRPCGuestService_CopyFromGuestStream interface { + drpc.Stream + Send(*CopyFromGuestResponse) error +} + +type drpcGuestService_CopyFromGuestStream struct { + drpc.Stream +} + +func (x *drpcGuestService_CopyFromGuestStream) Send(m *CopyFromGuestResponse) error { + return x.MsgSend(m, drpcEncoding_File_lib_guest_guest_proto{}) +} + +type DRPCGuestService_StatPathStream interface { + drpc.Stream + SendAndClose(*StatPathResponse) error +} + +type drpcGuestService_StatPathStream struct { + drpc.Stream +} + +func (x *drpcGuestService_StatPathStream) SendAndClose(m *StatPathResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_lib_guest_guest_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/lib/guest/guest_grpc.pb.go b/lib/guest/guest_grpc.pb.go deleted file mode 100644 index d656cb60..00000000 --- a/lib/guest/guest_grpc.pb.go +++ /dev/null @@ -1,238 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.6.0 -// - protoc v3.21.12 -// source: lib/guest/guest.proto - -package guest - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - GuestService_Exec_FullMethodName = "/guest.GuestService/Exec" - GuestService_CopyToGuest_FullMethodName = "/guest.GuestService/CopyToGuest" - GuestService_CopyFromGuest_FullMethodName = "/guest.GuestService/CopyFromGuest" - GuestService_StatPath_FullMethodName = "/guest.GuestService/StatPath" -) - -// GuestServiceClient is the client API for GuestService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// GuestService provides remote operations in guest VMs -type GuestServiceClient interface { - // Exec executes a command with bidirectional streaming - Exec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecRequest, ExecResponse], error) - // CopyToGuest streams file data to the guest filesystem - CopyToGuest(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[CopyToGuestRequest, CopyToGuestResponse], error) - // CopyFromGuest streams file data from the guest filesystem - CopyFromGuest(ctx context.Context, in *CopyFromGuestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CopyFromGuestResponse], error) - // StatPath returns information about a path in the guest filesystem - StatPath(ctx context.Context, in *StatPathRequest, opts ...grpc.CallOption) (*StatPathResponse, error) -} - -type guestServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewGuestServiceClient(cc grpc.ClientConnInterface) GuestServiceClient { - return &guestServiceClient{cc} -} - -func (c *guestServiceClient) Exec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecRequest, ExecResponse], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &GuestService_ServiceDesc.Streams[0], GuestService_Exec_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[ExecRequest, ExecResponse]{ClientStream: stream} - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type GuestService_ExecClient = grpc.BidiStreamingClient[ExecRequest, ExecResponse] - -func (c *guestServiceClient) CopyToGuest(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[CopyToGuestRequest, CopyToGuestResponse], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &GuestService_ServiceDesc.Streams[1], GuestService_CopyToGuest_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[CopyToGuestRequest, CopyToGuestResponse]{ClientStream: stream} - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type GuestService_CopyToGuestClient = grpc.ClientStreamingClient[CopyToGuestRequest, CopyToGuestResponse] - -func (c *guestServiceClient) CopyFromGuest(ctx context.Context, in *CopyFromGuestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CopyFromGuestResponse], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &GuestService_ServiceDesc.Streams[2], GuestService_CopyFromGuest_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[CopyFromGuestRequest, CopyFromGuestResponse]{ClientStream: stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type GuestService_CopyFromGuestClient = grpc.ServerStreamingClient[CopyFromGuestResponse] - -func (c *guestServiceClient) StatPath(ctx context.Context, in *StatPathRequest, opts ...grpc.CallOption) (*StatPathResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(StatPathResponse) - err := c.cc.Invoke(ctx, GuestService_StatPath_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// GuestServiceServer is the server API for GuestService service. -// All implementations must embed UnimplementedGuestServiceServer -// for forward compatibility. -// -// GuestService provides remote operations in guest VMs -type GuestServiceServer interface { - // Exec executes a command with bidirectional streaming - Exec(grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error - // CopyToGuest streams file data to the guest filesystem - CopyToGuest(grpc.ClientStreamingServer[CopyToGuestRequest, CopyToGuestResponse]) error - // CopyFromGuest streams file data from the guest filesystem - CopyFromGuest(*CopyFromGuestRequest, grpc.ServerStreamingServer[CopyFromGuestResponse]) error - // StatPath returns information about a path in the guest filesystem - StatPath(context.Context, *StatPathRequest) (*StatPathResponse, error) - mustEmbedUnimplementedGuestServiceServer() -} - -// UnimplementedGuestServiceServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedGuestServiceServer struct{} - -func (UnimplementedGuestServiceServer) Exec(grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error { - return status.Error(codes.Unimplemented, "method Exec not implemented") -} -func (UnimplementedGuestServiceServer) CopyToGuest(grpc.ClientStreamingServer[CopyToGuestRequest, CopyToGuestResponse]) error { - return status.Error(codes.Unimplemented, "method CopyToGuest not implemented") -} -func (UnimplementedGuestServiceServer) CopyFromGuest(*CopyFromGuestRequest, grpc.ServerStreamingServer[CopyFromGuestResponse]) error { - return status.Error(codes.Unimplemented, "method CopyFromGuest not implemented") -} -func (UnimplementedGuestServiceServer) StatPath(context.Context, *StatPathRequest) (*StatPathResponse, error) { - return nil, status.Error(codes.Unimplemented, "method StatPath not implemented") -} -func (UnimplementedGuestServiceServer) mustEmbedUnimplementedGuestServiceServer() {} -func (UnimplementedGuestServiceServer) testEmbeddedByValue() {} - -// UnsafeGuestServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to GuestServiceServer will -// result in compilation errors. -type UnsafeGuestServiceServer interface { - mustEmbedUnimplementedGuestServiceServer() -} - -func RegisterGuestServiceServer(s grpc.ServiceRegistrar, srv GuestServiceServer) { - // If the following call panics, it indicates UnimplementedGuestServiceServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&GuestService_ServiceDesc, srv) -} - -func _GuestService_Exec_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(GuestServiceServer).Exec(&grpc.GenericServerStream[ExecRequest, ExecResponse]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type GuestService_ExecServer = grpc.BidiStreamingServer[ExecRequest, ExecResponse] - -func _GuestService_CopyToGuest_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(GuestServiceServer).CopyToGuest(&grpc.GenericServerStream[CopyToGuestRequest, CopyToGuestResponse]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type GuestService_CopyToGuestServer = grpc.ClientStreamingServer[CopyToGuestRequest, CopyToGuestResponse] - -func _GuestService_CopyFromGuest_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(CopyFromGuestRequest) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(GuestServiceServer).CopyFromGuest(m, &grpc.GenericServerStream[CopyFromGuestRequest, CopyFromGuestResponse]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type GuestService_CopyFromGuestServer = grpc.ServerStreamingServer[CopyFromGuestResponse] - -func _GuestService_StatPath_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(StatPathRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GuestServiceServer).StatPath(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: GuestService_StatPath_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GuestServiceServer).StatPath(ctx, req.(*StatPathRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// GuestService_ServiceDesc is the grpc.ServiceDesc for GuestService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var GuestService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "guest.GuestService", - HandlerType: (*GuestServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "StatPath", - Handler: _GuestService_StatPath_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "Exec", - Handler: _GuestService_Exec_Handler, - ServerStreams: true, - ClientStreams: true, - }, - { - StreamName: "CopyToGuest", - Handler: _GuestService_CopyToGuest_Handler, - ClientStreams: true, - }, - { - StreamName: "CopyFromGuest", - Handler: _GuestService_CopyFromGuest_Handler, - ServerStreams: true, - }, - }, - Metadata: "lib/guest/guest.proto", -} diff --git a/lib/images/systemd.go b/lib/images/systemd.go new file mode 100644 index 00000000..661477fe --- /dev/null +++ b/lib/images/systemd.go @@ -0,0 +1,43 @@ +package images + +import "strings" + +// IsSystemdImage checks if the image's CMD indicates it wants systemd as init. +// Detection is based on the effective command (entrypoint + cmd), not whether +// systemd is installed in the image. +// +// Returns true if the image's command is: +// - /sbin/init +// - /lib/systemd/systemd +// - /usr/lib/systemd/systemd +// - Any path ending in /init +func IsSystemdImage(entrypoint, cmd []string) bool { + // Combine to get the actual command that will run + effective := append(entrypoint, cmd...) + if len(effective) == 0 { + return false + } + + first := effective[0] + + // Match specific systemd/init paths + systemdPaths := []string{ + "/sbin/init", + "/lib/systemd/systemd", + "/usr/lib/systemd/systemd", + } + for _, p := range systemdPaths { + if first == p { + return true + } + } + + // Match any absolute path ending in /init (e.g., /usr/sbin/init) + // Only match absolute paths to avoid false positives like "./init" + if strings.HasPrefix(first, "/") && strings.HasSuffix(first, "/init") { + return true + } + + return false +} + diff --git a/lib/images/systemd_test.go b/lib/images/systemd_test.go new file mode 100644 index 00000000..8b555873 --- /dev/null +++ b/lib/images/systemd_test.go @@ -0,0 +1,91 @@ +package images + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsSystemdImage(t *testing.T) { + tests := []struct { + name string + entrypoint []string + cmd []string + expected bool + }{ + { + name: "empty entrypoint and cmd", + entrypoint: nil, + cmd: nil, + expected: false, + }, + { + name: "/sbin/init as cmd", + entrypoint: nil, + cmd: []string{"/sbin/init"}, + expected: true, + }, + { + name: "/lib/systemd/systemd as cmd", + entrypoint: nil, + cmd: []string{"/lib/systemd/systemd"}, + expected: true, + }, + { + name: "/usr/lib/systemd/systemd as cmd", + entrypoint: nil, + cmd: []string{"/usr/lib/systemd/systemd"}, + expected: true, + }, + { + name: "path ending in /init", + entrypoint: nil, + cmd: []string{"/usr/sbin/init"}, + expected: true, + }, + { + name: "regular command (nginx)", + entrypoint: []string{"nginx"}, + cmd: []string{"-g", "daemon off;"}, + expected: false, + }, + { + name: "regular command (python)", + entrypoint: []string{"/usr/bin/python3"}, + cmd: []string{"app.py"}, + expected: false, + }, + { + name: "entrypoint with systemd", + entrypoint: []string{"/lib/systemd/systemd"}, + cmd: nil, + expected: true, + }, + { + name: "entrypoint with init", + entrypoint: []string{"/sbin/init"}, + cmd: nil, + expected: true, + }, + { + name: "shell script named init should not match", + entrypoint: nil, + cmd: []string{"./init"}, + expected: false, + }, + { + name: "bash command should not match", + entrypoint: nil, + cmd: []string{"/bin/bash", "-c", "init"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsSystemdImage(tt.entrypoint, tt.cmd) + assert.Equal(t, tt.expected, result) + }) + } +} + diff --git a/lib/instances/configdisk.go b/lib/instances/configdisk.go index fb9305ac..ba53ef7b 100644 --- a/lib/instances/configdisk.go +++ b/lib/instances/configdisk.go @@ -151,6 +151,15 @@ GUEST_DNS="%s" volumeSection = volumeLines.String() } + // Determine init mode based on image CMD + // If the image's command is /sbin/init or /lib/systemd/systemd, use systemd mode + initModeSection := "" + if images.IsSystemdImage(imageInfo.Entrypoint, imageInfo.Cmd) { + initModeSection = "\n# Init mode (auto-detected from image CMD)\nINIT_MODE=\"systemd\"\n" + } else { + initModeSection = "\n# Init mode\nINIT_MODE=\"exec\"\n" + } + // Generate script as a readable template block // ENTRYPOINT and CMD contain shell-quoted arrays that will be eval'd in init script := fmt.Sprintf(`#!/bin/sh @@ -162,7 +171,7 @@ CMD="%s" WORKDIR=%s # Environment variables -%s%s%s%s`, +%s%s%s%s%s`, inst.Id, entrypoint, cmd, @@ -171,6 +180,7 @@ WORKDIR=%s networkSection, volumeSection, gpuSection, + initModeSection, ) return script diff --git a/lib/system/README.md b/lib/system/README.md index 68629f03..59e8ce2c 100644 --- a/lib/system/README.md +++ b/lib/system/README.md @@ -5,10 +5,11 @@ Manages versioned kernel and initrd files for Cloud Hypervisor VMs. ## Features - **Automatic Downloads**: Kernel downloaded from Cloud Hypervisor releases on first use -- **Automatic Build**: Initrd built from busybox + custom init script +- **Automatic Build**: Initrd built from Alpine base + Go init binary + guest-agent - **Versioned**: Side-by-side support for multiple kernel/initrd versions - **Zero Docker**: Uses OCI directly (reuses image manager infrastructure) - **Zero Image Modifications**: All init logic in initrd, OCI images used as-is +- **Dual Mode Support**: Exec mode (container-like) and systemd mode (full VM) ## Architecture @@ -54,15 +55,27 @@ Instance B (running): kernel v6.12.9, initrd v1.0.0 Both work independently ``` -## Init Script Consolidation +## Go Init Binary -All init logic moved from app rootfs to initrd: +The init binary (`lib/system/init/`) is a Go program that runs as PID 1 in the guest VM. +It replaces the previous shell-based init script with cleaner logic and structured logging. **Initrd handles:** - ✅ Mount overlay filesystem - ✅ Mount and source config disk - ✅ Network configuration (if enabled) -- ✅ Execute container entrypoint +- ✅ Load GPU drivers (if GPU attached) +- ✅ Mount volumes +- ✅ Auto-detect systemd images from CMD +- ✅ Execute container entrypoint (exec mode) +- ✅ Hand off to systemd via pivot_root (systemd mode) + +**Two boot modes:** +- **Exec mode** (default): Init binary is PID 1, runs entrypoint as child process +- **Systemd mode** (auto-detected): Uses pivot_root to hand off to systemd as PID 1 + +**Systemd detection:** If image CMD is `/sbin/init`, `/lib/systemd/systemd`, or similar, +hypeman automatically uses systemd mode. **Result:** OCI images require **zero modifications** - no `/init` script needed! @@ -107,9 +120,11 @@ Example URLs: ## Initrd Build Process -1. **Pull busybox** (using image manager's OCI client) -2. **Inject init script** (comprehensive, handles all init logic) -3. **Package as cpio.gz** (initramfs format) +1. **Pull Alpine base** (using image manager's OCI client) +2. **Add guest-agent binary** (embedded, runs in guest for exec/shell) +3. **Add init binary** (embedded Go binary, runs as PID 1) +4. **Add NVIDIA modules** (optional, for GPU passthrough) +5. **Package as cpio.gz** (initramfs format) **Build tools required:** `find`, `cpio`, `gzip` (standard Unix tools) @@ -136,21 +151,18 @@ var KernelDownloadURLs = map[KernelVersion]map[string]string{ var DefaultKernelVersion = KernelV6_12_10 ``` -### New Initrd Version +### Updating the Init Binary -```go -// lib/system/versions.go +The init binary is in `lib/system/init/`. After making changes: -const ( - InitrdV1_1_0 InitrdVersion = "v1.1.0" // Add constant -) +1. Build the init binary (statically linked for Alpine): + ```bash + make build-init + ``` -// lib/system/init_script.go -// Update GenerateInitScript() if init logic changes +2. The binary is embedded via `lib/system/init_binary.go` -// Update default -var DefaultInitrdVersion = InitrdV1_1_0 -``` +3. The initrd hash includes the binary, so it will auto-rebuild on next startup ## Testing @@ -167,7 +179,22 @@ go test ./lib/system/... | File | Size | Purpose | |------|------|---------| | kernel/*/vmlinux | ~70MB | Cloud Hypervisor optimized kernel | -| initrd/*/initrd | ~1-2MB | Busybox + comprehensive init script | +| initrd/*/initrd | ~5-10MB | Alpine base + Go init binary + guest-agent | Files downloaded/built once per version, reused for all instances using that version. +## Init Binary Package Structure + +``` +lib/system/init/ + main.go # Entry point, orchestrates boot + mount.go # Mount operations (proc, sys, dev, overlay) + config.go # Parse config disk + network.go # Network configuration + drivers.go # GPU driver loading + volumes.go # Volume mounting + mode_exec.go # Exec mode: run entrypoint + mode_systemd.go # Systemd mode: pivot_root + exec init + logger.go # Human-readable logging to hypeman operations log +``` + diff --git a/lib/system/guest_agent/cp.go b/lib/system/guest_agent/cp.go index 19c681e8..8c9d5f7f 100644 --- a/lib/system/guest_agent/cp.go +++ b/lib/system/guest_agent/cp.go @@ -14,7 +14,7 @@ import ( ) // CopyToGuest handles copying files to the guest filesystem -func (s *guestServer) CopyToGuest(stream pb.GuestService_CopyToGuestServer) error { +func (s *guestServer) CopyToGuest(stream pb.DRPCGuestService_CopyToGuestStream) error { log.Printf("[guest-agent] new copy-to-guest stream") // Receive start request @@ -154,7 +154,7 @@ func (s *guestServer) CopyToGuest(stream pb.GuestService_CopyToGuestServer) erro } // CopyFromGuest handles copying files from the guest filesystem -func (s *guestServer) CopyFromGuest(req *pb.CopyFromGuestRequest, stream pb.GuestService_CopyFromGuestServer) error { +func (s *guestServer) CopyFromGuest(req *pb.CopyFromGuestRequest, stream pb.DRPCGuestService_CopyFromGuestStream) error { log.Printf("[guest-agent] copy-from-guest: path=%s follow_links=%v", req.Path, req.FollowLinks) // Stat the source path @@ -186,7 +186,7 @@ func (s *guestServer) CopyFromGuest(req *pb.CopyFromGuestRequest, stream pb.Gues } // copyFromGuestFile streams a single file -func (s *guestServer) copyFromGuestFile(fullPath, relativePath string, info os.FileInfo, followLinks bool, stream pb.GuestService_CopyFromGuestServer, isFinal bool) error { +func (s *guestServer) copyFromGuestFile(fullPath, relativePath string, info os.FileInfo, followLinks bool, stream pb.DRPCGuestService_CopyFromGuestStream, isFinal bool) error { if relativePath == "" { relativePath = filepath.Base(fullPath) } @@ -288,7 +288,7 @@ func (s *guestServer) copyFromGuestFile(fullPath, relativePath string, info os.F } // copyFromGuestDir walks a directory and streams all files -func (s *guestServer) copyFromGuestDir(rootPath string, followLinks bool, stream pb.GuestService_CopyFromGuestServer) error { +func (s *guestServer) copyFromGuestDir(rootPath string, followLinks bool, stream pb.DRPCGuestService_CopyFromGuestStream) error { // Collect all entries first to know which is final type entry struct { fullPath string diff --git a/lib/system/guest_agent/exec.go b/lib/system/guest_agent/exec.go index b48fd842..2340b2aa 100644 --- a/lib/system/guest_agent/exec.go +++ b/lib/system/guest_agent/exec.go @@ -15,7 +15,7 @@ import ( ) // Exec handles command execution with bidirectional streaming -func (s *guestServer) Exec(stream pb.GuestService_ExecServer) error { +func (s *guestServer) Exec(stream pb.DRPCGuestService_ExecStream) error { log.Printf("[guest-agent] new exec stream") // Receive start request @@ -52,7 +52,7 @@ func (s *guestServer) Exec(stream pb.GuestService_ExecServer) error { } // executeNoTTY executes command without TTY -func (s *guestServer) executeNoTTY(ctx context.Context, stream pb.GuestService_ExecServer, start *pb.ExecStart) error { +func (s *guestServer) executeNoTTY(ctx context.Context, stream pb.DRPCGuestService_ExecStream, start *pb.ExecStart) error { // Run command directly - guest-agent is already running in container namespace if len(start.Command) == 0 { return fmt.Errorf("empty command") @@ -160,7 +160,7 @@ func (s *guestServer) executeNoTTY(ctx context.Context, stream pb.GuestService_E } // executeTTY executes command with TTY -func (s *guestServer) executeTTY(ctx context.Context, stream pb.GuestService_ExecServer, start *pb.ExecStart) error { +func (s *guestServer) executeTTY(ctx context.Context, stream pb.DRPCGuestService_ExecStream, start *pb.ExecStart) error { // Run command directly with PTY - guest-agent is already running in container namespace // This ensures PTY and shell are in the same namespace, fixing Ctrl+C signal handling if len(start.Command) == 0 { diff --git a/lib/system/guest_agent/main.go b/lib/system/guest_agent/main.go index 6cb2f817..a0715088 100644 --- a/lib/system/guest_agent/main.go +++ b/lib/system/guest_agent/main.go @@ -1,17 +1,19 @@ package main import ( + "context" "log" "time" "github.com/mdlayher/vsock" pb "github.com/onkernel/hypeman/lib/guest" - "google.golang.org/grpc" + "storj.io/drpc/drpcmux" + "storj.io/drpc/drpcserver" ) -// guestServer implements the gRPC GuestService +// guestServer implements the DRPC GuestService type guestServer struct { - pb.UnimplementedGuestServiceServer + pb.DRPCGuestServiceUnimplementedServer } func main() { @@ -35,12 +37,25 @@ func main() { log.Println("[guest-agent] listening on vsock port 2222") - // Create gRPC server - grpcServer := grpc.NewServer() - pb.RegisterGuestServiceServer(grpcServer, &guestServer{}) + // Create DRPC server + mux := drpcmux.New() + if err := pb.DRPCRegisterGuestService(mux, &guestServer{}); err != nil { + log.Fatalf("[guest-agent] failed to register service: %v", err) + } + + server := drpcserver.New(mux) - // Serve gRPC over vsock - if err := grpcServer.Serve(l); err != nil { - log.Fatalf("[guest-agent] gRPC server failed: %v", err) + // Serve DRPC over vsock - accept connections in a loop + for { + conn, err := l.Accept() + if err != nil { + log.Printf("[guest-agent] accept error: %v", err) + continue + } + go func() { + if err := server.ServeOne(context.Background(), conn); err != nil { + log.Printf("[guest-agent] connection error: %v", err) + } + }() } } diff --git a/lib/system/guest_agent_binary.go b/lib/system/guest_agent_binary.go index 78a5b7b3..57d69722 100644 --- a/lib/system/guest_agent_binary.go +++ b/lib/system/guest_agent_binary.go @@ -6,4 +6,3 @@ import _ "embed" // This is built by the Makefile before the main binary is compiled //go:embed guest_agent/guest-agent var GuestAgentBinary []byte - diff --git a/lib/system/init/config.go b/lib/system/init/config.go new file mode 100644 index 00000000..a6ef1e07 --- /dev/null +++ b/lib/system/init/config.go @@ -0,0 +1,194 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" +) + +// Config holds the parsed configuration from the config disk. +type Config struct { + // Container execution parameters + Entrypoint string + Cmd string + Workdir string + + // Environment variables + Env map[string]string + + // Network configuration + NetworkEnabled bool + GuestIP string + GuestCIDR string + GuestGW string + GuestDNS string + + // GPU passthrough + HasGPU bool + + // Volume mounts (format: "device:path:mode[:overlay_device] ...") + VolumeMounts []VolumeMount + + // Init mode: "exec" (default) or "systemd" + InitMode string +} + +// VolumeMount represents a volume mount configuration. +type VolumeMount struct { + Device string + Path string + Mode string // "ro", "rw", or "overlay" + OverlayDevice string // Only used for overlay mode +} + +// readConfig mounts and reads the config disk, parsing the shell configuration. +func readConfig(log *Logger) (*Config, error) { + const configMount = "/mnt/config" + const configFile = "/mnt/config/config.sh" + + // Create mount point + if err := os.MkdirAll(configMount, 0755); err != nil { + return nil, fmt.Errorf("mkdir config mount: %w", err) + } + + // Mount config disk (/dev/vdc) read-only + cmd := exec.Command("/bin/mount", "-o", "ro", "/dev/vdc", configMount) + if output, err := cmd.CombinedOutput(); err != nil { + return nil, fmt.Errorf("mount config disk: %s: %s", err, output) + } + log.Info("config", "mounted config disk") + + // Read and parse config.sh + cfg, err := parseConfigFile(configFile) + if err != nil { + return nil, fmt.Errorf("parse config: %w", err) + } + log.Info("config", "parsed configuration") + + return cfg, nil +} + +// parseConfigFile parses a shell-style configuration file. +// It handles simple KEY=VALUE and KEY="VALUE" assignments. +func parseConfigFile(path string) (*Config, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + cfg := &Config{ + Env: make(map[string]string), + InitMode: "exec", // Default to exec mode + } + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip comments and empty lines + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // Handle export statements + if strings.HasPrefix(line, "export ") { + line = strings.TrimPrefix(line, "export ") + } + + // Parse KEY=VALUE + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := unquote(strings.TrimSpace(parts[1])) + + switch key { + case "ENTRYPOINT": + cfg.Entrypoint = value + case "CMD": + cfg.Cmd = value + case "WORKDIR": + cfg.Workdir = value + case "GUEST_IP": + cfg.GuestIP = value + cfg.NetworkEnabled = true + case "GUEST_CIDR": + cfg.GuestCIDR = value + case "GUEST_GW": + cfg.GuestGW = value + case "GUEST_DNS": + cfg.GuestDNS = value + case "HAS_GPU": + cfg.HasGPU = value == "1" + case "VOLUME_MOUNTS": + cfg.VolumeMounts = parseVolumeMounts(value) + case "INIT_MODE": + cfg.InitMode = value + default: + // Treat as environment variable + cfg.Env[key] = value + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return cfg, nil +} + +// parseVolumeMounts parses the VOLUME_MOUNTS string. +// Format: "device:path:mode[:overlay_device] device:path:mode ..." +func parseVolumeMounts(s string) []VolumeMount { + if s == "" { + return nil + } + + var mounts []VolumeMount + for _, vol := range strings.Fields(s) { + parts := strings.Split(vol, ":") + if len(parts) < 3 { + continue + } + + mount := VolumeMount{ + Device: parts[0], + Path: parts[1], + Mode: parts[2], + } + + if len(parts) >= 4 { + mount.OverlayDevice = parts[3] + } + + mounts = append(mounts, mount) + } + + return mounts +} + +// unquote removes surrounding quotes from a string. +// Handles both single and double quotes. +func unquote(s string) string { + if len(s) < 2 { + return s + } + + // Handle double quotes + if s[0] == '"' && s[len(s)-1] == '"' { + return s[1 : len(s)-1] + } + + // Handle single quotes + if s[0] == '\'' && s[len(s)-1] == '\'' { + return s[1 : len(s)-1] + } + + return s +} + diff --git a/lib/system/init/drivers.go b/lib/system/init/drivers.go new file mode 100644 index 00000000..935e0f9e --- /dev/null +++ b/lib/system/init/drivers.go @@ -0,0 +1,190 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// loadGPUDrivers loads NVIDIA kernel modules for GPU passthrough. +func loadGPUDrivers(log *Logger) error { + log.Info("gpu", "loading NVIDIA kernel modules") + + // Find kernel version directory + modules, err := os.ReadDir("/lib/modules") + if err != nil { + return fmt.Errorf("read /lib/modules: %w", err) + } + + if len(modules) == 0 { + return fmt.Errorf("no kernel modules found") + } + + kver := modules[0].Name() + gpuDir := filepath.Join("/lib/modules", kver, "kernel/drivers/gpu") + + if _, err := os.Stat(gpuDir); err != nil { + return fmt.Errorf("GPU modules not found for kernel %s", kver) + } + + // Load modules in order (dependencies first) + moduleOrder := []string{ + "nvidia.ko", + "nvidia-uvm.ko", + "nvidia-modeset.ko", + "nvidia-drm.ko", + } + + for _, mod := range moduleOrder { + modPath := filepath.Join(gpuDir, mod) + if _, err := os.Stat(modPath); err != nil { + log.Error("gpu", fmt.Sprintf("%s not found", mod), nil) + continue + } + + args := []string{modPath} + // nvidia-drm needs modeset=1 + if mod == "nvidia-drm.ko" { + args = append(args, "modeset=1") + } + + cmd := exec.Command("/sbin/insmod", args...) + if output, err := cmd.CombinedOutput(); err != nil { + log.Error("gpu", fmt.Sprintf("insmod %s failed", mod), fmt.Errorf("%s", output)) + } + } + + log.Info("gpu", fmt.Sprintf("loaded NVIDIA modules for kernel %s", kver)) + + // Create device nodes using nvidia-modprobe if available + if err := createNvidiaDevices(log); err != nil { + log.Error("gpu", "failed to create device nodes", err) + } + + // Inject NVIDIA userspace driver libraries into container rootfs + if err := injectNvidiaLibraries(log); err != nil { + log.Error("gpu", "failed to inject driver libraries", err) + } + + return nil +} + +// createNvidiaDevices creates NVIDIA device nodes. +func createNvidiaDevices(log *Logger) error { + // Try nvidia-modprobe first (the official NVIDIA utility) + if _, err := os.Stat("/usr/bin/nvidia-modprobe"); err == nil { + log.Info("gpu", "running nvidia-modprobe to create device nodes") + + cmd := exec.Command("/usr/bin/nvidia-modprobe") + cmd.CombinedOutput() + + cmd = exec.Command("/usr/bin/nvidia-modprobe", "-u", "-c=0") + cmd.CombinedOutput() + + return nil + } + + // Fallback: Manual device node creation + log.Info("gpu", "nvidia-modprobe not found, creating device nodes manually") + + // Read major numbers from /proc/devices + data, err := os.ReadFile("/proc/devices") + if err != nil { + return err + } + + lines := strings.Split(string(data), "\n") + var nvidiaMajor, uvmMajor string + + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 2 { + if fields[1] == "nvidia-frontend" || fields[1] == "nvidia" { + nvidiaMajor = fields[0] + } else if fields[1] == "nvidia-uvm" { + uvmMajor = fields[0] + } + } + } + + if nvidiaMajor != "" { + exec.Command("/bin/mknod", "-m", "666", "/dev/nvidiactl", "c", nvidiaMajor, "255").Run() + exec.Command("/bin/mknod", "-m", "666", "/dev/nvidia0", "c", nvidiaMajor, "0").Run() + log.Info("gpu", fmt.Sprintf("created /dev/nvidiactl and /dev/nvidia0 (major %s)", nvidiaMajor)) + } + + if uvmMajor != "" { + exec.Command("/bin/mknod", "-m", "666", "/dev/nvidia-uvm", "c", uvmMajor, "0").Run() + exec.Command("/bin/mknod", "-m", "666", "/dev/nvidia-uvm-tools", "c", uvmMajor, "1").Run() + log.Info("gpu", fmt.Sprintf("created /dev/nvidia-uvm* (major %s)", uvmMajor)) + } + + return nil +} + +// injectNvidiaLibraries injects NVIDIA userspace driver libraries into the container rootfs. +// This allows containers to use standard CUDA images without bundled drivers. +func injectNvidiaLibraries(log *Logger) error { + srcDir := "/usr/lib/nvidia" + if _, err := os.Stat(srcDir); err != nil { + return nil // No driver libraries to inject + } + + log.Info("gpu", "injecting NVIDIA driver libraries into container") + + // Determine library path based on architecture + var libDst string + if runtime.GOARCH == "arm64" { + libDst = "/overlay/newroot/usr/lib/aarch64-linux-gnu" + } else { + libDst = "/overlay/newroot/usr/lib/x86_64-linux-gnu" + } + binDst := "/overlay/newroot/usr/bin" + + if err := os.MkdirAll(libDst, 0755); err != nil { + return err + } + if err := os.MkdirAll(binDst, 0755); err != nil { + return err + } + + // Copy all driver libraries + libs, _ := filepath.Glob(filepath.Join(srcDir, "*.so.*")) + for _, lib := range libs { + libname := filepath.Base(lib) + data, err := os.ReadFile(lib) + if err != nil { + continue + } + os.WriteFile(filepath.Join(libDst, libname), data, 0755) + + // Create standard symlinks + base := strings.Split(libname, ".so.")[0] + os.Symlink(libname, filepath.Join(libDst, base+".so.1")) + os.Symlink(base+".so.1", filepath.Join(libDst, base+".so")) + } + + // Copy nvidia-smi and nvidia-modprobe binaries + for _, bin := range []string{"nvidia-smi", "nvidia-modprobe"} { + srcPath := filepath.Join("/usr/bin", bin) + if data, err := os.ReadFile(srcPath); err == nil { + os.WriteFile(filepath.Join(binDst, bin), data, 0755) + } + } + + // Update ldconfig cache + exec.Command("/usr/sbin/chroot", "/overlay/newroot", "ldconfig").Run() + + // Read driver version + version := "unknown" + if data, err := os.ReadFile(filepath.Join(srcDir, "version")); err == nil { + version = strings.TrimSpace(string(data)) + } + + log.Info("gpu", fmt.Sprintf("injected NVIDIA driver libraries (version: %s)", version)) + return nil +} + diff --git a/lib/system/init/init.sh b/lib/system/init/init.sh new file mode 100644 index 00000000..06b739cd --- /dev/null +++ b/lib/system/init/init.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Minimal init wrapper that sets up environment before running Go init +# The Go runtime needs /proc and /dev to exist during initialization +# +# This pattern is used by other Go-based init systems: +# - u-root (github.com/u-root/u-root) - uses assembly stub for early mount +# - LinuxKit (github.com/linuxkit/linuxkit) - similar shell wrapper approach +# - gokrazy (github.com/gokrazy/gokrazy) - mounts filesystems before Go starts + +# Mount essential filesystems BEFORE running Go binary +mkdir -p /proc /sys /dev +mount -t proc proc /proc +mount -t sysfs sysfs /sys +mount -t devtmpfs devtmpfs /dev + +# Now exec the Go init binary (it will take over as PID 1) +exec /init.bin "$@" diff --git a/lib/system/init/logger.go b/lib/system/init/logger.go new file mode 100644 index 00000000..6d0a5217 --- /dev/null +++ b/lib/system/init/logger.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +// Logger provides human-readable structured logging for the init process. +// Logs are written to serial console. +type Logger struct { + console *os.File +} + +// NewLogger creates a new logger that writes to serial console. +func NewLogger() *Logger { + l := &Logger{} + + // Open serial console for output + // ttyS0 for x86_64, ttyAMA0 for ARM64 (PL011 UART) + if f, err := os.OpenFile("/dev/ttyAMA0", os.O_WRONLY, 0); err == nil { + l.console = f + } else if f, err := os.OpenFile("/dev/ttyS0", os.O_WRONLY, 0); err == nil { + l.console = f + } else { + // Fallback to stdout + l.console = os.Stdout + } + return l +} + +// SetConsole sets the serial console for output. +func (l *Logger) SetConsole(path string) { + if f, err := os.OpenFile(path, os.O_WRONLY, 0); err == nil { + l.console = f + } +} + +// Info logs an informational message. +// Format: 2024-12-23T10:15:30Z [INFO] [phase] message +func (l *Logger) Info(phase, msg string) { + ts := time.Now().UTC().Format(time.RFC3339) + line := fmt.Sprintf("%s [INFO] [%s] %s\n", ts, phase, msg) + l.write(line) +} + +// Error logs an error message. +// Format: 2024-12-23T10:15:30Z [ERROR] [phase] message: error +func (l *Logger) Error(phase, msg string, err error) { + ts := time.Now().UTC().Format(time.RFC3339) + var line string + if err != nil { + line = fmt.Sprintf("%s [ERROR] [%s] %s: %v\n", ts, phase, msg, err) + } else { + line = fmt.Sprintf("%s [ERROR] [%s] %s\n", ts, phase, msg) + } + l.write(line) +} + +// Infof logs a formatted informational message. +func (l *Logger) Infof(phase, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + l.Info(phase, msg) +} + +// write outputs a log line to serial console. +func (l *Logger) write(line string) { + if l.console != nil { + l.console.WriteString(line) + } +} diff --git a/lib/system/init/main.go b/lib/system/init/main.go new file mode 100644 index 00000000..ca25e426 --- /dev/null +++ b/lib/system/init/main.go @@ -0,0 +1,95 @@ +// Package main implements the hypeman init binary that runs as PID 1 in guest VMs. +// +// This binary replaces the shell-based init script with a Go program that provides: +// - Human-readable structured logging +// - Clean separation of boot phases +// - Support for both exec mode (container-like) and systemd mode (full VM) +// +// Note: This binary is called by init.sh wrapper which mounts /proc, /sys, /dev +// before the Go runtime starts (Go requires these during initialization). +package main + +import ( + "os" + "os/exec" +) + +func main() { + log := NewLogger() + log.Info("boot", "init starting") + + // Phase 1: Mount additional filesystems (proc/sys/dev already mounted by init.sh) + if err := mountEssentials(log); err != nil { + log.Error("mount", "failed to mount essentials", err) + dropToShell() + } + + // Phase 2: Setup overlay rootfs + if err := setupOverlay(log); err != nil { + log.Error("overlay", "failed to setup overlay", err) + dropToShell() + } + + // Phase 3: Read and parse config + cfg, err := readConfig(log) + if err != nil { + log.Error("config", "failed to read config", err) + dropToShell() + } + + // Phase 4: Configure network (shared between modes) + if cfg.NetworkEnabled { + if err := configureNetwork(log, cfg); err != nil { + log.Error("network", "failed to configure network", err) + // Continue anyway - network isn't always required + } + } + + // Phase 5: Load GPU drivers if needed + if cfg.HasGPU { + if err := loadGPUDrivers(log); err != nil { + log.Error("gpu", "failed to load GPU drivers", err) + // Continue anyway + } + } + + // Phase 6: Mount volumes + if len(cfg.VolumeMounts) > 0 { + if err := mountVolumes(log, cfg); err != nil { + log.Error("volumes", "failed to mount volumes", err) + // Continue anyway + } + } + + // Phase 7: Bind mount filesystems to new root + if err := bindMountsToNewRoot(log); err != nil { + log.Error("bind", "failed to bind mounts", err) + dropToShell() + } + + // Phase 8: Copy guest-agent to target location + if err := copyGuestAgent(log); err != nil { + log.Error("agent", "failed to copy guest-agent", err) + // Continue anyway - exec will still work, just no remote access + } + + // Phase 9: Mode-specific execution + if cfg.InitMode == "systemd" { + log.Info("mode", "entering systemd mode") + runSystemdMode(log, cfg) + } else { + log.Info("mode", "entering exec mode") + runExecMode(log, cfg) + } +} + +// dropToShell drops to an interactive shell for debugging when boot fails +func dropToShell() { + cmd := exec.Command("/bin/sh", "-i") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + os.Exit(1) +} + diff --git a/lib/system/init/mode_exec.go b/lib/system/init/mode_exec.go new file mode 100644 index 00000000..c7152cae --- /dev/null +++ b/lib/system/init/mode_exec.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "syscall" +) + +// runExecMode runs the container in exec mode (default). +// This is the Docker-like behavior where: +// - The init binary remains PID 1 +// - Guest-agent runs as a background process +// - The container entrypoint runs as a child process +// - When the entrypoint exits, the VM exits +func runExecMode(log *Logger, cfg *Config) { + const newroot = "/overlay/newroot" + + // Set up environment + os.Setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") + os.Setenv("HOME", "/root") + + // Start guest-agent in background inside the container namespace + log.Info("exec", "starting guest-agent in background") + agentCmd := exec.Command("/usr/sbin/chroot", newroot, "/opt/hypeman/guest-agent") + agentCmd.Stdout = os.Stdout + agentCmd.Stderr = os.Stderr + if err := agentCmd.Start(); err != nil { + log.Error("exec", "failed to start guest-agent", err) + } + + // Build the entrypoint command + workdir := cfg.Workdir + if workdir == "" { + workdir = "/" + } + + entrypoint := cfg.Entrypoint + cmd := cfg.Cmd + + log.Info("exec", fmt.Sprintf("workdir=%s entrypoint=%s cmd=%s", workdir, entrypoint, cmd)) + + // Construct the shell command to run + // ENTRYPOINT and CMD are shell-safe quoted strings from config.sh + shellCmd := fmt.Sprintf("cd %s && exec %s %s", workdir, entrypoint, cmd) + + log.Info("exec", "launching entrypoint") + + // Run the entrypoint + appCmd := exec.Command("/usr/sbin/chroot", newroot, "/bin/sh", "-c", shellCmd) + appCmd.Stdin = os.Stdin + appCmd.Stdout = os.Stdout + appCmd.Stderr = os.Stderr + + // Set up environment for the app + appCmd.Env = buildEnv(cfg.Env) + + if err := appCmd.Start(); err != nil { + log.Error("exec", "failed to start entrypoint", err) + dropToShell() + } + + log.Info("exec", fmt.Sprintf("container app started (PID %d)", appCmd.Process.Pid)) + + // Wait for app to exit + err := appCmd.Wait() + exitCode := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } + } + + log.Info("exec", fmt.Sprintf("app exited with code %d", exitCode)) + + // Wait for guest-agent (keeps init alive, prevents kernel panic) + // The guest-agent runs forever, so this effectively keeps the VM alive + // until it's explicitly terminated + if agentCmd.Process != nil { + agentCmd.Wait() + } + + // Exit with the app's exit code + syscall.Exit(exitCode) +} + +// buildEnv constructs environment variables from the config. +func buildEnv(env map[string]string) []string { + result := []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOME=/root", + } + + for k, v := range env { + result = append(result, fmt.Sprintf("%s=%s", k, v)) + } + + return result +} + diff --git a/lib/system/init/mode_systemd.go b/lib/system/init/mode_systemd.go new file mode 100644 index 00000000..6f042a11 --- /dev/null +++ b/lib/system/init/mode_systemd.go @@ -0,0 +1,90 @@ +package main + +import ( + "os" + "syscall" +) + +// runSystemdMode hands off control to systemd. +// This is used when the image's CMD is /sbin/init or /lib/systemd/systemd. +// The init binary: +// 1. Injects the hypeman-agent.service unit +// 2. Uses chroot to switch to the container rootfs +// 3. Execs /sbin/init (systemd) which becomes the new PID 1 +func runSystemdMode(log *Logger, cfg *Config) { + const newroot = "/overlay/newroot" + + // Inject hypeman-agent.service + log.Info("systemd", "injecting hypeman-agent.service") + if err := injectAgentService(newroot); err != nil { + log.Error("systemd", "failed to inject service", err) + // Continue anyway - VM will work, just without agent + } + + // Change root to the new filesystem using chroot + log.Info("systemd", "executing chroot") + if err := syscall.Chroot(newroot); err != nil { + log.Error("systemd", "chroot failed", err) + dropToShell() + } + + // Change to new root directory + if err := os.Chdir("/"); err != nil { + log.Error("systemd", "chdir / failed", err) + dropToShell() + } + + // Exec systemd - this replaces the current process + log.Info("systemd", "exec /sbin/init") + + // syscall.Exec replaces the current process with the new one + // /sbin/init is typically a symlink to /lib/systemd/systemd + err := syscall.Exec("/sbin/init", []string{"/sbin/init"}, os.Environ()) + if err != nil { + log.Error("systemd", "exec /sbin/init failed", err) + dropToShell() + } +} + +// injectAgentService creates the systemd service unit for the hypeman guest-agent. +func injectAgentService(newroot string) error { + serviceContent := `[Unit] +Description=Hypeman Guest Agent +After=network.target +Wants=network.target + +[Service] +Type=simple +ExecStart=/opt/hypeman/guest-agent +Restart=always +RestartSec=3 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +` + + serviceDir := newroot + "/etc/systemd/system" + wantsDir := serviceDir + "/multi-user.target.wants" + + // Create directories + if err := os.MkdirAll(serviceDir, 0755); err != nil { + return err + } + if err := os.MkdirAll(wantsDir, 0755); err != nil { + return err + } + + // Write service file + servicePath := serviceDir + "/hypeman-agent.service" + if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil { + return err + } + + // Enable the service by creating a symlink in wants directory + symlinkPath := wantsDir + "/hypeman-agent.service" + // Use relative path for the symlink + return os.Symlink("../hypeman-agent.service", symlinkPath) +} + diff --git a/lib/system/init/mount.go b/lib/system/init/mount.go new file mode 100644 index 00000000..3dcee32a --- /dev/null +++ b/lib/system/init/mount.go @@ -0,0 +1,210 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "syscall" + "time" +) + +// mountEssentials mounts additional filesystems needed for boot. +// Note: /proc, /sys, /dev are already mounted by the init.sh wrapper script +// before the Go binary runs (the Go runtime needs them during initialization). +// This function mounts: +// - /dev/pts (pseudo-terminals) +// - /dev/shm (shared memory) +func mountEssentials(log *Logger) error { + // Create mount points for pts and shm (proc/sys/dev already exist from wrapper) + for _, dir := range []string{"/dev/pts", "/dev/shm"} { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("mkdir %s: %w", dir, err) + } + } + + // Mount devpts for PTY support (needed for guest-agent and interactive shells) + if err := syscall.Mount("devpts", "/dev/pts", "devpts", 0, ""); err != nil { + return fmt.Errorf("mount /dev/pts: %w", err) + } + + // Set permissions on /dev/shm + if err := os.Chmod("/dev/shm", 01777); err != nil { + return fmt.Errorf("chmod /dev/shm: %w", err) + } + + log.Info("mount", "mounted devpts/shm") + + // Set up serial console now that /dev is mounted + // ttyS0 for x86_64, ttyAMA0 for ARM64 (PL011 UART) + if _, err := os.Stat("/dev/ttyAMA0"); err == nil { + log.SetConsole("/dev/ttyAMA0") + redirectToConsole("/dev/ttyAMA0") + } else if _, err := os.Stat("/dev/ttyS0"); err == nil { + log.SetConsole("/dev/ttyS0") + redirectToConsole("/dev/ttyS0") + } + + log.Info("mount", "redirected to serial console") + + return nil +} + +// setupOverlay sets up the overlay filesystem: +// - /dev/vda: readonly rootfs (ext4) +// - /dev/vdb: writable overlay disk (ext4) +// - /overlay/newroot: merged overlay filesystem +func setupOverlay(log *Logger) error { + // Wait for block devices to be ready + time.Sleep(500 * time.Millisecond) + + // Create mount points + for _, dir := range []string{"/lower", "/overlay"} { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("mkdir %s: %w", dir, err) + } + } + + // Mount readonly rootfs from /dev/vda (ext4 filesystem) + if err := mount("/dev/vda", "/lower", "ext4", "ro"); err != nil { + return fmt.Errorf("mount rootfs: %w", err) + } + log.Info("overlay", "mounted rootfs from /dev/vda") + + // Mount writable overlay disk from /dev/vdb + if err := mount("/dev/vdb", "/overlay", "ext4", ""); err != nil { + return fmt.Errorf("mount overlay disk: %w", err) + } + + // Create overlay directories + for _, dir := range []string{"/overlay/upper", "/overlay/work", "/overlay/newroot"} { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("mkdir %s: %w", dir, err) + } + } + log.Info("overlay", "mounted overlay disk from /dev/vdb") + + // Create overlay filesystem + if err := mountOverlay("/lower", "/overlay/upper", "/overlay/work", "/overlay/newroot"); err != nil { + return fmt.Errorf("mount overlay: %w", err) + } + log.Info("overlay", "created overlay filesystem") + + return nil +} + +// bindMountsToNewRoot bind-mounts essential filesystems to the new root. +// Uses bind mounts instead of move so that the original /dev remains populated +// for processes running in the initrd namespace. +func bindMountsToNewRoot(log *Logger) error { + newroot := "/overlay/newroot" + + // Create mount points in new root + for _, dir := range []string{"proc", "sys", "dev", "dev/pts"} { + if err := os.MkdirAll(newroot+"/"+dir, 0755); err != nil { + return fmt.Errorf("mkdir %s: %w", dir, err) + } + } + + // Bind mount filesystems + mounts := []struct{ src, dst string }{ + {"/proc", newroot + "/proc"}, + {"/sys", newroot + "/sys"}, + {"/dev", newroot + "/dev"}, + {"/dev/pts", newroot + "/dev/pts"}, + } + + for _, m := range mounts { + if err := bindMount(m.src, m.dst); err != nil { + return fmt.Errorf("bind mount %s: %w", m.src, err) + } + } + + log.Info("bind", "bound mounts to new root") + + // Set up /dev symlinks for process substitution inside the container + symlinks := []struct{ target, link string }{ + {"/proc/self/fd", newroot + "/dev/fd"}, + {"/proc/self/fd/0", newroot + "/dev/stdin"}, + {"/proc/self/fd/1", newroot + "/dev/stdout"}, + {"/proc/self/fd/2", newroot + "/dev/stderr"}, + } + + for _, s := range symlinks { + os.Remove(s.link) // Remove if exists + os.Symlink(s.target, s.link) + } + + return nil +} + +// mount executes a mount command +func mount(source, target, fstype, options string) error { + args := []string{"-t", fstype} + if options != "" { + args = append(args, "-o", options) + } + args = append(args, source, target) + + cmd := exec.Command("/bin/mount", args...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %s", err, output) + } + return nil +} + +// mountOverlay creates an overlay filesystem +func mountOverlay(lower, upper, work, target string) error { + options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lower, upper, work) + cmd := exec.Command("/bin/mount", "-t", "overlay", "-o", options, "overlay", target) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %s", err, output) + } + return nil +} + +// bindMount performs a bind mount +func bindMount(source, target string) error { + cmd := exec.Command("/bin/mount", "--bind", source, target) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %s", err, output) + } + return nil +} + +// redirectToConsole redirects stdout/stderr to the serial console +func redirectToConsole(device string) { + f, err := os.OpenFile(device, os.O_WRONLY, 0) + if err != nil { + return + } + os.Stdout = f + os.Stderr = f +} + +// copyGuestAgent copies the guest-agent binary to the target location in the new root. +func copyGuestAgent(log *Logger) error { + const ( + src = "/usr/local/bin/guest-agent" + dst = "/overlay/newroot/opt/hypeman/guest-agent" + ) + + // Create target directory + if err := os.MkdirAll("/overlay/newroot/opt/hypeman", 0755); err != nil { + return fmt.Errorf("mkdir: %w", err) + } + + // Read source binary + data, err := os.ReadFile(src) + if err != nil { + return fmt.Errorf("read source: %w", err) + } + + // Write to destination + if err := os.WriteFile(dst, data, 0755); err != nil { + return fmt.Errorf("write destination: %w", err) + } + + log.Info("agent", "copied guest-agent to /opt/hypeman/") + return nil +} + diff --git a/lib/system/init/network.go b/lib/system/init/network.go new file mode 100644 index 00000000..d9d3d008 --- /dev/null +++ b/lib/system/init/network.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +// configureNetwork sets up networking in the guest VM. +// This is done from the initrd before pivot_root so it works for both exec and systemd modes. +func configureNetwork(log *Logger, cfg *Config) error { + // Bring up loopback interface + if err := runIP("link", "set", "lo", "up"); err != nil { + return fmt.Errorf("bring up lo: %w", err) + } + + // Add IP address to eth0 + addr := fmt.Sprintf("%s/%s", cfg.GuestIP, cfg.GuestCIDR) + if err := runIP("addr", "add", addr, "dev", "eth0"); err != nil { + return fmt.Errorf("add IP address: %w", err) + } + + // Bring up eth0 + if err := runIP("link", "set", "eth0", "up"); err != nil { + return fmt.Errorf("bring up eth0: %w", err) + } + + // Add default route + if err := runIP("route", "add", "default", "via", cfg.GuestGW); err != nil { + return fmt.Errorf("add default route: %w", err) + } + + // Configure DNS in the new root + resolvConf := fmt.Sprintf("nameserver %s\n", cfg.GuestDNS) + resolvPath := "/overlay/newroot/etc/resolv.conf" + + // Ensure /etc exists + if err := os.MkdirAll("/overlay/newroot/etc", 0755); err != nil { + return fmt.Errorf("mkdir /etc: %w", err) + } + + if err := os.WriteFile(resolvPath, []byte(resolvConf), 0644); err != nil { + return fmt.Errorf("write resolv.conf: %w", err) + } + + log.Info("network", fmt.Sprintf("configured eth0 with %s", addr)) + return nil +} + +// runIP executes an 'ip' command with the given arguments. +func runIP(args ...string) error { + cmd := exec.Command("/sbin/ip", args...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %s", err, output) + } + return nil +} + diff --git a/lib/system/init/volumes.go b/lib/system/init/volumes.go new file mode 100644 index 00000000..2a523932 --- /dev/null +++ b/lib/system/init/volumes.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// mountVolumes mounts attached volumes according to the configuration. +// Supports three modes: ro (read-only), rw (read-write), and overlay. +func mountVolumes(log *Logger, cfg *Config) error { + log.Info("volumes", "mounting volumes") + + for _, vol := range cfg.VolumeMounts { + mountPath := filepath.Join("/overlay/newroot", vol.Path) + + // Create mount point + if err := os.MkdirAll(mountPath, 0755); err != nil { + log.Error("volumes", fmt.Sprintf("mkdir %s failed", vol.Path), err) + continue + } + + switch vol.Mode { + case "overlay": + if err := mountVolumeOverlay(log, vol, mountPath); err != nil { + log.Error("volumes", fmt.Sprintf("mount overlay %s failed", vol.Path), err) + } + case "ro": + if err := mountVolumeReadOnly(log, vol, mountPath); err != nil { + log.Error("volumes", fmt.Sprintf("mount ro %s failed", vol.Path), err) + } + default: // "rw" + if err := mountVolumeReadWrite(log, vol, mountPath); err != nil { + log.Error("volumes", fmt.Sprintf("mount rw %s failed", vol.Path), err) + } + } + } + + return nil +} + +// mountVolumeOverlay mounts a volume in overlay mode. +// Uses the base device as read-only lower layer and overlay device for writable upper layer. +func mountVolumeOverlay(log *Logger, vol VolumeMount, mountPath string) error { + baseName := filepath.Base(vol.Path) + baseMount := fmt.Sprintf("/mnt/vol-base-%s", baseName) + overlayMount := fmt.Sprintf("/mnt/vol-overlay-%s", baseName) + + // Create mount points + if err := os.MkdirAll(baseMount, 0755); err != nil { + return err + } + if err := os.MkdirAll(overlayMount, 0755); err != nil { + return err + } + + // Mount base volume read-only (noload to skip journal recovery) + cmd := exec.Command("/bin/mount", "-t", "ext4", "-o", "ro,noload", vol.Device, baseMount) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("mount base: %s: %s", err, output) + } + + // Mount overlay disk (writable) + cmd = exec.Command("/bin/mount", "-t", "ext4", vol.OverlayDevice, overlayMount) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("mount overlay disk: %s: %s", err, output) + } + + // Create overlay directories + upperDir := filepath.Join(overlayMount, "upper") + workDir := filepath.Join(overlayMount, "work") + os.MkdirAll(upperDir, 0755) + os.MkdirAll(workDir, 0755) + + // Create overlayfs + options := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", baseMount, upperDir, workDir) + cmd = exec.Command("/bin/mount", "-t", "overlay", "-o", options, "overlay", mountPath) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("mount overlay: %s: %s", err, output) + } + + log.Info("volumes", fmt.Sprintf("mounted %s at %s (overlay via %s)", vol.Device, vol.Path, vol.OverlayDevice)) + return nil +} + +// mountVolumeReadOnly mounts a volume in read-only mode. +func mountVolumeReadOnly(log *Logger, vol VolumeMount, mountPath string) error { + // Use noload to skip journal recovery for multi-attach safety + cmd := exec.Command("/bin/mount", "-t", "ext4", "-o", "ro,noload", vol.Device, mountPath) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %s", err, output) + } + + log.Info("volumes", fmt.Sprintf("mounted %s at %s (ro)", vol.Device, vol.Path)) + return nil +} + +// mountVolumeReadWrite mounts a volume in read-write mode. +func mountVolumeReadWrite(log *Logger, vol VolumeMount, mountPath string) error { + cmd := exec.Command("/bin/mount", "-t", "ext4", vol.Device, mountPath) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s: %s", err, output) + } + + log.Info("volumes", fmt.Sprintf("mounted %s at %s (rw)", vol.Device, vol.Path)) + return nil +} + diff --git a/lib/system/init_binary.go b/lib/system/init_binary.go new file mode 100644 index 00000000..ad378a67 --- /dev/null +++ b/lib/system/init_binary.go @@ -0,0 +1,11 @@ +package system + +import _ "embed" + +// InitBinary contains the embedded init binary for guest VMs. +// This is built by the Makefile before the main binary is compiled. +// The init binary is a statically-linked Go program that runs as PID 1 in the guest VM. +// It matches the architecture of the host (VMs run on the same arch as the host). +// +//go:embed init/init +var InitBinary []byte diff --git a/lib/system/init_script.go b/lib/system/init_script.go deleted file mode 100644 index 9e9b397a..00000000 --- a/lib/system/init_script.go +++ /dev/null @@ -1,294 +0,0 @@ -package system - -// GenerateInitScript returns the comprehensive init script for initrd -// This consolidates ALL init logic - no modifications to OCI images needed -// -// The script: -// 1. Mounts essential filesystems (proc, sys, dev) -// 2. Sets up overlay filesystem (lowerdir=rootfs, upperdir=overlay disk) -// 3. Mounts and sources config disk (/dev/vdc) -// 4. Loads NVIDIA kernel modules (if HAS_GPU=1 in config.sh) -// 5. Configures networking (if enabled) -// 6. Executes container entrypoint -// -// GPU support: When HAS_GPU=1 is set in the instance's config.sh, the init script -// will load NVIDIA kernel modules before launching the container entrypoint. -func GenerateInitScript() string { - return `#!/bin/sh -set -xe - -echo "overlay-init: START" > /dev/kmsg - -# Create mount points -mkdir -p /proc /sys /dev - -# Mount essential filesystems -# devtmpfs handles /dev population (null, zero, vsock, etc.) automatically -mount -t proc none /proc -mount -t sysfs none /sys -mount -t devtmpfs none /dev - -# Setup PTY support (needed for guest-agent and interactive shells) -mkdir -p /dev/pts /dev/shm -mount -t devpts devpts /dev/pts -chmod 1777 /dev/shm - -echo "overlay-init: mounted proc/sys/dev" > /dev/kmsg - -# Redirect all output to serial console -# ttyS0 for x86_64, ttyAMA0 for ARM64 (PL011 UART) -if [ -e /dev/ttyAMA0 ]; then - exec >/dev/ttyAMA0 2>&1 -else - exec >/dev/ttyS0 2>&1 -fi - -echo "overlay-init: redirected to serial console" - -# Wait for block devices to be ready -sleep 0.5 - -# Mount readonly rootfs from /dev/vda (ext4 filesystem) -mkdir -p /lower -mount -t ext4 -o ro /dev/vda /lower -echo "overlay-init: mounted rootfs from /dev/vda" - -# Mount writable overlay disk from /dev/vdb -mkdir -p /overlay -mount -t ext4 /dev/vdb /overlay -mkdir -p /overlay/upper /overlay/work /overlay/newroot -echo "overlay-init: mounted overlay disk from /dev/vdb" - -# Create overlay filesystem -mount -t overlay \ - -o lowerdir=/lower,upperdir=/overlay/upper,workdir=/overlay/work \ - overlay /overlay/newroot -echo "overlay-init: created overlay filesystem" - -# Mount config disk (/dev/vdc) -mkdir -p /mnt/config -mount -o ro /dev/vdc /mnt/config -echo "overlay-init: mounted config disk" - -# Source configuration -if [ -f /mnt/config/config.sh ]; then - . /mnt/config/config.sh - echo "overlay-init: sourced config" -else - echo "overlay-init: ERROR - config.sh not found!" - /bin/sh -i - exit 1 -fi - -# Load NVIDIA kernel modules for GPU passthrough (if HAS_GPU=1) -if [ "${HAS_GPU:-0}" = "1" ]; then - echo "overlay-init: loading NVIDIA kernel modules for GPU passthrough" - if [ -d /lib/modules ]; then - # Find the kernel version directory - KVER=$(ls /lib/modules/ 2>/dev/null | head -1) - if [ -n "$KVER" ] && [ -d "/lib/modules/$KVER/kernel/drivers/gpu" ]; then - # Load modules in order (dependencies first) - insmod /lib/modules/$KVER/kernel/drivers/gpu/nvidia.ko 2>&1 || echo "overlay-init: nvidia.ko load failed" - insmod /lib/modules/$KVER/kernel/drivers/gpu/nvidia-uvm.ko 2>&1 || echo "overlay-init: nvidia-uvm.ko load failed" - insmod /lib/modules/$KVER/kernel/drivers/gpu/nvidia-modeset.ko 2>&1 || echo "overlay-init: nvidia-modeset.ko load failed" - insmod /lib/modules/$KVER/kernel/drivers/gpu/nvidia-drm.ko modeset=1 2>&1 || echo "overlay-init: nvidia-drm.ko load failed" - echo "overlay-init: NVIDIA modules loaded for kernel $KVER" - - # Use nvidia-modprobe to create device nodes with correct major/minor numbers. - # nvidia-modprobe is the official NVIDIA utility that: - # 1. Loads kernel modules if needed (already done above) - # 2. Creates /dev/nvidiactl and /dev/nvidia0 with correct permissions - # 3. Creates /dev/nvidia-uvm and /dev/nvidia-uvm-tools - if [ -x /usr/bin/nvidia-modprobe ]; then - echo "overlay-init: running nvidia-modprobe to create device nodes" - /usr/bin/nvidia-modprobe 2>&1 || echo "overlay-init: nvidia-modprobe failed" - /usr/bin/nvidia-modprobe -u -c=0 2>&1 || echo "overlay-init: nvidia-modprobe -u failed" - echo "overlay-init: nvidia-modprobe completed" - ls -la /dev/nvidia* 2>/dev/null || true - else - echo "overlay-init: nvidia-modprobe not found, falling back to manual mknod" - # Fallback: Manual device node creation - NVIDIA_MAJOR=$(awk '/nvidia-frontend|^[0-9]+ nvidia$/ {print $1}' /proc/devices 2>/dev/null | head -1) - NVIDIA_UVM_MAJOR=$(awk '/nvidia-uvm/ {print $1}' /proc/devices 2>/dev/null) - - if [ -n "$NVIDIA_MAJOR" ]; then - mknod -m 666 /dev/nvidiactl c $NVIDIA_MAJOR 255 - mknod -m 666 /dev/nvidia0 c $NVIDIA_MAJOR 0 - echo "overlay-init: created /dev/nvidiactl and /dev/nvidia0 (major $NVIDIA_MAJOR)" - fi - - if [ -n "$NVIDIA_UVM_MAJOR" ]; then - mknod -m 666 /dev/nvidia-uvm c $NVIDIA_UVM_MAJOR 0 - mknod -m 666 /dev/nvidia-uvm-tools c $NVIDIA_UVM_MAJOR 1 - echo "overlay-init: created /dev/nvidia-uvm* (major $NVIDIA_UVM_MAJOR)" - fi - fi - else - echo "overlay-init: NVIDIA modules not found in /lib/modules/$KVER" - fi - else - echo "overlay-init: /lib/modules not found, skipping NVIDIA module loading" - fi - - # Inject NVIDIA userspace driver libraries into container rootfs - # This allows containers to use standard CUDA images without bundled drivers - # See lib/devices/GPU.md for documentation - if [ -d /usr/lib/nvidia ]; then - echo "overlay-init: injecting NVIDIA driver libraries into container" - - DRIVER_VERSION=$(cat /usr/lib/nvidia/version 2>/dev/null || echo "unknown") - # Determine library path based on architecture - if [ "$(uname -m)" = "aarch64" ]; then - LIB_DST="/overlay/newroot/usr/lib/aarch64-linux-gnu" - else - LIB_DST="/overlay/newroot/usr/lib/x86_64-linux-gnu" - fi - BIN_DST="/overlay/newroot/usr/bin" - - mkdir -p "$LIB_DST" "$BIN_DST" - - # Copy all driver libraries and create symlinks - for lib in /usr/lib/nvidia/*.so.*; do - if [ -f "$lib" ]; then - libname=$(basename "$lib") - cp "$lib" "$LIB_DST/" - - # Create standard symlinks: libfoo.so.VERSION -> libfoo.so.1 -> libfoo.so - base=$(echo "$libname" | sed 's/\.so\..*//') - ln -sf "$libname" "$LIB_DST/${base}.so.1" 2>/dev/null || true - ln -sf "${base}.so.1" "$LIB_DST/${base}.so" 2>/dev/null || true - fi - done - - # Copy nvidia-smi and nvidia-modprobe binaries - for bin in nvidia-smi nvidia-modprobe; do - if [ -x /usr/bin/$bin ]; then - cp /usr/bin/$bin "$BIN_DST/" - fi - done - - # Update ldconfig cache so applications can find the libraries - chroot /overlay/newroot ldconfig 2>/dev/null || true - - echo "overlay-init: NVIDIA driver libraries injected (version: $DRIVER_VERSION)" - fi -fi - -# Mount attached volumes (from config: VOLUME_MOUNTS="device:path:mode[:overlay_device] ...") -# Modes: ro (read-only), rw (read-write), overlay (base ro + per-instance overlay) -if [ -n "${VOLUME_MOUNTS:-}" ]; then - echo "overlay-init: mounting volumes" - for vol in $VOLUME_MOUNTS; do - device=$(echo "$vol" | cut -d: -f1) - path=$(echo "$vol" | cut -d: -f2) - mode=$(echo "$vol" | cut -d: -f3) - - # Create mount point in overlay - mkdir -p "/overlay/newroot${path}" - - if [ "$mode" = "overlay" ]; then - # Overlay mode: mount base read-only, create overlayfs with per-instance writable layer - overlay_device=$(echo "$vol" | cut -d: -f4) - - # Create temp mount points for base and overlay disk. - # These persist for the lifetime of the VM but are NOT leaked - they exist inside - # the ephemeral guest rootfs (which is itself an overlayfs) and are destroyed - # when the VM terminates along with all guest state. - base_mount="/mnt/vol-base-$(basename "$path")" - overlay_mount="/mnt/vol-overlay-$(basename "$path")" - mkdir -p "$base_mount" "$overlay_mount" - - # Mount base volume read-only (noload to skip journal recovery) - mount -t ext4 -o ro,noload "$device" "$base_mount" - - # Mount overlay disk (writable) - mount -t ext4 "$overlay_device" "$overlay_mount" - mkdir -p "$overlay_mount/upper" "$overlay_mount/work" - - # Create overlayfs combining base (lower) and instance overlay (upper) - mount -t overlay \ - -o "lowerdir=$base_mount,upperdir=$overlay_mount/upper,workdir=$overlay_mount/work" \ - overlay "/overlay/newroot${path}" - - echo "overlay-init: mounted volume $device at $path (overlay via $overlay_device)" - elif [ "$mode" = "ro" ]; then - # Read-only mount (noload to skip journal recovery for multi-attach safety) - mount -t ext4 -o ro,noload "$device" "/overlay/newroot${path}" - echo "overlay-init: mounted volume $device at $path (ro)" - else - # Read-write mount - mount -t ext4 "$device" "/overlay/newroot${path}" - echo "overlay-init: mounted volume $device at $path (rw)" - fi - done -fi - -# Prepare new root mount points -# We use bind mounts instead of move so that the original /dev remains populated -# for processes running in the initrd namespace (like guest-agent). -mkdir -p /overlay/newroot/proc -mkdir -p /overlay/newroot/sys -mkdir -p /overlay/newroot/dev -mkdir -p /overlay/newroot/dev/pts - -mount --bind /proc /overlay/newroot/proc -mount --bind /sys /overlay/newroot/sys -mount --bind /dev /overlay/newroot/dev -mount --bind /dev/pts /overlay/newroot/dev/pts - -echo "overlay-init: bound mounts to new root" - -# Set up /dev symlinks for process substitution inside the container -chroot /overlay/newroot ln -sf /proc/self/fd /dev/fd 2>/dev/null || true -chroot /overlay/newroot ln -sf /proc/self/fd/0 /dev/stdin 2>/dev/null || true -chroot /overlay/newroot ln -sf /proc/self/fd/1 /dev/stdout 2>/dev/null || true -chroot /overlay/newroot ln -sf /proc/self/fd/2 /dev/stderr 2>/dev/null || true - -# Configure network from initrd (using busybox ip, not container's ip) -# Network interfaces are shared, so we can configure them from here -if [ -n "${GUEST_IP:-}" ]; then - echo "overlay-init: configuring network" - ip link set lo up - ip addr add ${GUEST_IP}/${GUEST_CIDR} dev eth0 - ip link set eth0 up - ip route add default via ${GUEST_GW} - echo "nameserver ${GUEST_DNS}" > /overlay/newroot/etc/resolv.conf - echo "overlay-init: network configured - IP: ${GUEST_IP}/${GUEST_CIDR}" -fi - -# Set PATH for initrd tools -export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' -export HOME='/root' - -# Copy guest-agent into container rootfs and start it in container namespace -# This way the PTY and shell run in the same namespace, fixing signal handling -echo "overlay-init: copying guest-agent to container" -mkdir -p /overlay/newroot/usr/local/bin -cp /usr/local/bin/guest-agent /overlay/newroot/usr/local/bin/guest-agent - -# Start vsock guest agent inside the container namespace -echo "overlay-init: starting guest agent in container namespace" -chroot /overlay/newroot /usr/local/bin/guest-agent & - -echo "overlay-init: launching entrypoint" -echo "overlay-init: workdir=${WORKDIR:-/} entrypoint=${ENTRYPOINT} cmd=${CMD}" - -set +e - -# Construct the command string carefully -# ENTRYPOINT and CMD are shell-safe quoted strings from config.sh -eval "chroot /overlay/newroot /bin/sh -c \"cd ${WORKDIR:-/} && exec ${ENTRYPOINT} ${CMD}\"" & -APP_PID=$! - -echo "overlay-init: container app started (PID $APP_PID)" - -# Wait for app to exit -wait $APP_PID -APP_EXIT=$? - -echo "overlay-init: app exited with code $APP_EXIT" - -# Wait for all background jobs (guest-agent runs forever, keeping init alive) -# This prevents kernel panic from killing init (PID 1) -wait` -} diff --git a/lib/system/init_wrapper.go b/lib/system/init_wrapper.go new file mode 100644 index 00000000..16056f2d --- /dev/null +++ b/lib/system/init_wrapper.go @@ -0,0 +1,6 @@ +package system + +import _ "embed" + +//go:embed init/init.sh +var InitWrapper []byte diff --git a/lib/system/initrd.go b/lib/system/initrd.go index 3048b75b..567247d2 100644 --- a/lib/system/initrd.go +++ b/lib/system/initrd.go @@ -20,7 +20,7 @@ import ( const alpineBaseImage = "alpine:3.22" -// buildInitrd builds initrd from Alpine base + embedded guest-agent + generated init script +// buildInitrd builds initrd from Alpine base + embedded guest-agent + embedded init binary func (m *manager) buildInitrd(ctx context.Context, arch string) (string, error) { // Create temp directory for building tempDir, err := os.MkdirTemp("", "hypeman-initrd-*") @@ -67,11 +67,17 @@ func (m *manager) buildInitrd(ctx context.Context, arch string) (string, error) log.InfoContext(ctx, "skipping NVIDIA modules", "error", err) } - // Write generated init script - initScript := GenerateInitScript() - initPath := filepath.Join(rootfsDir, "init") - if err := os.WriteFile(initPath, []byte(initScript), 0755); err != nil { - return "", fmt.Errorf("write init script: %w", err) + // Write shell wrapper as /init (sets up /proc, /sys, /dev before Go runtime) + // The Go runtime needs these filesystems during initialization + initWrapperPath := filepath.Join(rootfsDir, "init") + if err := os.WriteFile(initWrapperPath, InitWrapper, 0755); err != nil { + return "", fmt.Errorf("write init wrapper: %w", err) + } + + // Write Go init binary as /init.bin (called by wrapper after setup) + initBinPath := filepath.Join(rootfsDir, "init.bin") + if err := os.WriteFile(initBinPath, InitBinary, 0755); err != nil { + return "", fmt.Errorf("write init binary: %w", err) } // Generate timestamp for this build @@ -89,7 +95,7 @@ func (m *manager) buildInitrd(ctx context.Context, arch string) (string, error) // Store hash for staleness detection hashPath := filepath.Join(filepath.Dir(outputPath), ".hash") - currentHash := computeInitrdHash() + currentHash := computeInitrdHash(arch) if err := os.WriteFile(hashPath, []byte(currentHash), 0644); err != nil { return "", fmt.Errorf("write hash file: %w", err) } @@ -117,7 +123,7 @@ func (m *manager) ensureInitrd(ctx context.Context) (string, error) { initrdPath := m.paths.SystemInitrdTimestamp(target, arch) if _, err := os.Stat(initrdPath); err == nil { // File exists, check if it's stale by comparing embedded binary hash - if !m.isInitrdStale(initrdPath) { + if !m.isInitrdStale(initrdPath, arch) { return initrdPath, nil } } @@ -133,7 +139,7 @@ func (m *manager) ensureInitrd(ctx context.Context) (string, error) { } // isInitrdStale checks if the initrd needs rebuilding by comparing hashes -func (m *manager) isInitrdStale(initrdPath string) bool { +func (m *manager) isInitrdStale(initrdPath, arch string) bool { // Read stored hash hashPath := filepath.Join(filepath.Dir(initrdPath), ".hash") storedHash, err := os.ReadFile(hashPath) @@ -143,22 +149,23 @@ func (m *manager) isInitrdStale(initrdPath string) bool { } // Compare with current hash - currentHash := computeInitrdHash() + currentHash := computeInitrdHash(arch) return string(storedHash) != currentHash } -// computeInitrdHash computes a hash of the embedded binary, init script, and NVIDIA assets -func computeInitrdHash() string { +// computeInitrdHash computes a hash of the embedded binaries and NVIDIA assets for a specific architecture +func computeInitrdHash(arch string) string { h := sha256.New() h.Write(GuestAgentBinary) - h.Write([]byte(GenerateInitScript())) + h.Write(InitBinary) + h.Write(InitWrapper) // Include NVIDIA driver version in hash so initrd is rebuilt when driver changes if ver, ok := NvidiaDriverVersion[DefaultKernelVersion]; ok { h.Write([]byte(ver)) } // Include driver libs URL so initrd is rebuilt when the libs tarball changes if archURLs, ok := NvidiaDriverLibURLs[DefaultKernelVersion]; ok { - if url, ok := archURLs["x86_64"]; ok { + if url, ok := archURLs[arch]; ok { h.Write([]byte(url)) } } diff --git a/lib/system/manager_test.go b/lib/system/manager_test.go index ce32df12..ea91ae6b 100644 --- a/lib/system/manager_test.go +++ b/lib/system/manager_test.go @@ -58,16 +58,9 @@ func TestEnsureSystemFiles(t *testing.T) { require.NoError(t, err) } -func TestInitScriptGeneration(t *testing.T) { - script := GenerateInitScript() - - // Verify script contains essential components - assert.Contains(t, script, "#!/bin/sh") - assert.Contains(t, script, "mount -t overlay") - assert.Contains(t, script, "/dev/vda") // rootfs disk - assert.Contains(t, script, "/dev/vdb") // overlay disk - assert.Contains(t, script, "/dev/vdc") // config disk - assert.Contains(t, script, "guest-agent") // vsock guest agent - assert.Contains(t, script, "${ENTRYPOINT}") - assert.Contains(t, script, "wait $APP_PID") // Supervisor pattern +func TestInitBinaryEmbedded(t *testing.T) { + // Verify the init binary is embedded and has reasonable size + // The Go init binary should be at least 1MB when statically linked + assert.NotEmpty(t, InitBinary, "init binary should be embedded") + assert.Greater(t, len(InitBinary), 100000, "init binary should be at least 100KB") }