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 = 1024 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 DaemonConfig struct { RuntimeDir string LogLevel string FirecrackerBin string SSHKeyPath string NamegenPath string CustomizeScript string AutoStopStaleAfter time.Duration StatsPollInterval time.Duration MetricsPollInterval time.Duration BridgeName string BridgeIP string CIDR string DefaultDNS string DefaultImageName string DefaultRootfs string DefaultBaseRootfs string DefaultKernel string DefaultInitrd string DefaultModulesDir string DefaultPackagesFile 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"` KernelPath string `json:"kernel_path"` InitrdPath string `json:"initrd_path,omitempty"` ModulesDir string `json:"modules_dir,omitempty"` PackagesPath string `json:"packages_path,omitempty"` BuildSize string `json:"build_size,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"` } type VMRuntime struct { State VMState `json:"state"` PID int `json:"pid,omitempty"` GuestIP string `json:"guest_ip"` TapDevice string `json:"tap_device,omitempty"` APISockPath string `json:"api_sock_path,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"` BaseLoop string `json:"base_loop,omitempty"` COWLoop string `json:"cow_loop,omitempty"` DMName string `json:"dm_name,omitempty"` DMDev string `json:"dm_dev,omitempty"` 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 ImageBuildRequest struct { Name string BaseRootfs string Size string KernelPath string InitrdPath string ModulesDir string Docker bool } 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) } }