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 { LogLevel string WebListenAddr 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 } 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"` } 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"` 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"` 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 FromImage 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) } }