package model import ( "crypto/rand" "encoding/hex" "errors" "fmt" "strconv" "strings" "time" ) const ( DefaultBridgeName = "br-fc" DefaultBridgeIP = "172.16.0.1" DefaultCIDR = "24" DefaultDNS = "1.1.1.1" DefaultSystemOverlaySize = 8 * 1024 * 1024 * 1024 DefaultWorkDiskSize = 8 * 1024 * 1024 * 1024 DefaultMemoryMiB = 2048 DefaultVCPUCount = 2 DefaultStatsPollInterval = 10 * time.Second DefaultStaleSweepInterval = 1 * time.Minute DefaultMetricsPollInterval = 15 * time.Second MaxDiskBytes int64 = 128 * 1024 * 1024 * 1024 ) type VMState string const ( VMStateCreated VMState = "created" VMStateRunning VMState = "running" VMStateStopped VMState = "stopped" VMStateError VMState = "error" ) type GuestSessionStatus string const ( GuestSessionStatusStarting GuestSessionStatus = "starting" GuestSessionStatusRunning GuestSessionStatus = "running" GuestSessionStatusExited GuestSessionStatus = "exited" GuestSessionStatusFailed GuestSessionStatus = "failed" GuestSessionStatusStopping GuestSessionStatus = "stopping" ) type GuestSessionStdinMode string const ( GuestSessionStdinClosed GuestSessionStdinMode = "closed" GuestSessionStdinPipe GuestSessionStdinMode = "pipe" ) type DaemonConfig struct { LogLevel string FirecrackerBin string SSHKeyPath string AutoStopStaleAfter time.Duration StatsPollInterval time.Duration MetricsPollInterval time.Duration BridgeName string BridgeIP string CIDR string TapPoolSize int DefaultDNS string DefaultImageName string FileSync []FileSyncEntry VMDefaults VMDefaultsOverride } // FileSyncEntry is a user-declared host→guest file or directory copy // applied to each VM's work disk at vm create time. Host is expanded // against the host user's $HOME for "~/..."; Guest is expanded // against /root (banger VMs are single-user root). If the host path // is a directory, it's copied recursively; if it's a file, it's // copied as a file. Missing host paths are a soft skip (warned, not // fatal). Mode defaults to 0600 for files and 0755 for directories. type FileSyncEntry struct { Host string Guest string Mode string } type Image struct { ID string `json:"id"` Name string `json:"name"` Managed bool `json:"managed"` ArtifactDir string `json:"artifact_dir,omitempty"` RootfsPath string `json:"rootfs_path"` WorkSeedPath string `json:"work_seed_path,omitempty"` KernelPath string `json:"kernel_path"` InitrdPath string `json:"initrd_path,omitempty"` ModulesDir string `json:"modules_dir,omitempty"` BuildSize string `json:"build_size,omitempty"` SeededSSHPublicKeyFingerprint string `json:"seeded_ssh_public_key_fingerprint,omitempty"` Docker bool `json:"docker"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type VMSpec struct { VCPUCount int `json:"vcpu_count"` MemoryMiB int `json:"memory_mib"` SystemOverlaySizeByte int64 `json:"system_overlay_size_bytes"` WorkDiskSizeBytes int64 `json:"work_disk_size_bytes"` NATEnabled bool `json:"nat_enabled"` } // VMRuntime holds the durable runtime state that the daemon needs // to reach a VM: identity, declared state, and deterministic derived // paths. Transient kernel/process handles (PID, tap, loop devices, // dm-snapshot names) live on VMHandles, NOT here — the daemon keeps // them in an in-memory cache backed by a per-VM handles.json scratch // file, so a daemon restart rebuilds them from OS state rather than // trusting whatever was last written into a SQLite column. // // Everything in VMRuntime is safe to persist: the paths are // deterministic from (VM ID, layout) and survive restart unchanged; // GuestIP and DNSName are assigned at create time and never move; // LastError carries the last failure message for debugging. State // mirrors VMRecord.State. type VMRuntime struct { State VMState `json:"state"` GuestIP string `json:"guest_ip"` APISockPath string `json:"api_sock_path,omitempty"` VSockPath string `json:"vsock_path,omitempty"` VSockCID uint32 `json:"vsock_cid,omitempty"` LogPath string `json:"log_path,omitempty"` MetricsPath string `json:"metrics_path,omitempty"` DNSName string `json:"dns_name,omitempty"` VMDir string `json:"vm_dir"` SystemOverlay string `json:"system_overlay_path"` WorkDiskPath string `json:"work_disk_path"` LastError string `json:"last_error,omitempty"` } type VMStats struct { CollectedAt time.Time `json:"collected_at,omitempty"` CPUPercent float64 `json:"cpu_percent,omitempty"` RSSBytes int64 `json:"rss_bytes,omitempty"` VSZBytes int64 `json:"vsz_bytes,omitempty"` SystemOverlayBytes int64 `json:"system_overlay_bytes,omitempty"` WorkDiskBytes int64 `json:"work_disk_bytes,omitempty"` MetricsRaw map[string]any `json:"metrics_raw,omitempty"` } type VMRecord struct { ID string `json:"id"` Name string `json:"name"` ImageID string `json:"image_id"` State VMState `json:"state"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` LastTouchedAt time.Time `json:"last_touched_at"` Spec VMSpec `json:"spec"` Runtime VMRuntime `json:"runtime"` Stats VMStats `json:"stats"` } type VMCreateRequest struct { Name string ImageName string VCPUCount int MemoryMiB int SystemOverlaySizeByte int64 WorkDiskSizeBytes int64 NATEnabled bool NoStart bool } type VMSetRequest struct { IDOrName string VCPUCount *int MemoryMiB *int WorkDiskSizeBytes *int64 NATEnabled *bool } type GuestSession struct { ID string `json:"id"` VMID string `json:"vm_id"` Name string `json:"name"` Backend string `json:"backend"` AttachBackend string `json:"attach_backend,omitempty"` AttachMode string `json:"attach_mode,omitempty"` Command string `json:"command"` Args []string `json:"args,omitempty"` CWD string `json:"cwd,omitempty"` Env map[string]string `json:"env,omitempty"` StdinMode GuestSessionStdinMode `json:"stdin_mode,omitempty"` Status GuestSessionStatus `json:"status"` ExitCode *int `json:"exit_code,omitempty"` GuestPID int `json:"guest_pid,omitempty"` GuestStateDir string `json:"guest_state_dir,omitempty"` StdoutLogPath string `json:"stdout_log_path,omitempty"` StderrLogPath string `json:"stderr_log_path,omitempty"` Tags map[string]string `json:"tags,omitempty"` LastError string `json:"last_error,omitempty"` Attachable bool `json:"attachable"` Reattachable bool `json:"reattachable"` LaunchStage string `json:"launch_stage,omitempty"` LaunchMessage string `json:"launch_message,omitempty"` LaunchRawLog string `json:"launch_raw_log,omitempty"` CreatedAt time.Time `json:"created_at"` StartedAt time.Time `json:"started_at,omitempty"` UpdatedAt time.Time `json:"updated_at"` EndedAt time.Time `json:"ended_at,omitempty"` } type WorkspacePrepareMode string const ( WorkspacePrepareModeShallowOverlay WorkspacePrepareMode = "shallow_overlay" WorkspacePrepareModeFullCopy WorkspacePrepareMode = "full_copy" WorkspacePrepareModeMetadataOnly WorkspacePrepareMode = "metadata_only" ) type WorkspacePrepareResult struct { VMID string `json:"vm_id"` SourcePath string `json:"source_path"` RepoRoot string `json:"repo_root"` RepoName string `json:"repo_name"` GuestPath string `json:"guest_path"` Mode WorkspacePrepareMode `json:"mode"` ReadOnly bool `json:"readonly"` HeadCommit string `json:"head_commit,omitempty"` CurrentBranch string `json:"current_branch,omitempty"` BranchName string `json:"branch_name,omitempty"` BaseCommit string `json:"base_commit,omitempty"` PreparedAt time.Time `json:"prepared_at"` } func Now() time.Time { return time.Now().UTC().Truncate(time.Second) } func NewID() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } func ParseSize(raw string) (int64, error) { if raw == "" { return 0, errors.New("size is required") } raw = strings.TrimSpace(strings.ToUpper(raw)) if raw == "" { return 0, errors.New("size is required") } unit := raw[len(raw)-1] multiplier := int64(1024 * 1024) number := raw switch unit { case 'K': multiplier = 1024 number = raw[:len(raw)-1] case 'M': multiplier = 1024 * 1024 number = raw[:len(raw)-1] case 'G': multiplier = 1024 * 1024 * 1024 number = raw[:len(raw)-1] default: if unit < '0' || unit > '9' { return 0, fmt.Errorf("unsupported size suffix: %q", string(unit)) } } value, err := strconv.ParseInt(number, 10, 64) if err != nil { return 0, fmt.Errorf("parse size %q: %w", raw, err) } result := value * multiplier if result <= 0 { return 0, fmt.Errorf("size must be positive: %q", raw) } if result > MaxDiskBytes { return 0, fmt.Errorf("size exceeds max of %d bytes", MaxDiskBytes) } return result, nil } func FormatSizeBytes(bytes int64) string { switch { case bytes%(1024*1024*1024) == 0: return fmt.Sprintf("%dG", bytes/(1024*1024*1024)) case bytes%(1024*1024) == 0: return fmt.Sprintf("%dM", bytes/(1024*1024)) case bytes%1024 == 0: return fmt.Sprintf("%dK", bytes/1024) default: return strconv.FormatInt(bytes, 10) } }