Stop assuming one workstation layout for runtime artifacts, mapdns, and host tooling. The daemon and shell helpers now use portable mapdns configuration, and runtime bundles can carry bundle.json metadata for their default kernel, initrd, modules, rootfs, and helper paths. Load bundle metadata through config with a legacy layout fallback, thread mapdns_bin/mapdns_data_file through the Go and shell paths, and add command-scoped preflight checks for VM start, NAT, image build, work-disk resize, and SSH so missing tools or artifacts fail with actionable errors. Update the runtime-bundle manifest, docs, and tests to match the new model. Verified with go test ./..., make build, and bash -n customize.sh interactive.sh dns.sh make-rootfs.sh verify.sh.
219 lines
6.2 KiB
Go
219 lines
6.2 KiB
Go
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
|
|
FirecrackerBin string
|
|
MapDNSBin string
|
|
MapDNSDataFile 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)
|
|
}
|
|
}
|