From 7128a22fdba96b479cd3cf298cc9db9ae0678c93 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:59:29 +1100 Subject: [PATCH] feat: detect CPU generation instead of vendor for instance types (#40) --- go.mod | 2 +- hive/daemon/daemon.go | 372 +++++++++++++++++++++++-------------- hive/daemon/daemon_test.go | 371 +++++++++++++++++++++++++----------- hive/vm/vm.go | 5 - 4 files changed, 494 insertions(+), 256 deletions(-) diff --git a/go.mod b/go.mod index 486f6cb67..4f0ad3487 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gofiber/fiber/v2 v2.52.9 github.com/google/uuid v1.6.0 github.com/kdomanski/iso9660 v0.4.0 + github.com/klauspost/cpuid/v2 v2.3.0 github.com/mulgadc/predastore v1.0.0 github.com/mulgadc/viperblock v1.0.10 github.com/nats-io/nats-server/v2 v2.11.6 @@ -69,7 +70,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/reedsolomon v1.12.6 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/hive/daemon/daemon.go b/hive/daemon/daemon.go index d5d82f85b..de85c0e47 100644 --- a/hive/daemon/daemon.go +++ b/hive/daemon/daemon.go @@ -27,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/gofiber/fiber/v2" + cpuid "github.com/klauspost/cpuid/v2" "github.com/mulgadc/hive/hive/awserrors" "github.com/mulgadc/hive/hive/config" handlers_ec2_account "github.com/mulgadc/hive/hive/handlers/ec2/account" @@ -126,33 +127,181 @@ type Daemon struct { mu sync.Mutex } -// cpuToVendor maps CPU model patterns to CPU vendors. -// Patterns are checked in order; more specific patterns should come first. -var cpuToVendor = []struct { - pattern string - vendor cpuVendor -}{ - {"EPYC", vendorAMD}, - {"Xeon", vendorIntel}, - {"Graviton", vendorARM}, - {"Apple", vendorARM}, - {"ARM", vendorARM}, - {"AMD", vendorAMD}, // catches Ryzen, Threadripper, etc. +// cpuGeneration represents a specific CPU microarchitecture generation +// and the AWS instance families it maps to. +type cpuGeneration struct { + name string // e.g. "Intel Ice Lake", "AMD Genoa" + families []string // e.g. ["t3", "c6i", "m6i", "r6i"] } -// vendorFromCPU returns the CPU vendor based on the CPU model string and architecture. -func vendorFromCPU(cpuModel, arch string) cpuVendor { - if arch == "arm64" { - return vendorARM +var ( + // Intel generations + genIntelBroadwell = cpuGeneration{"Intel Broadwell", []string{"t2", "c4", "m4", "r4"}} + genIntelSkylake = cpuGeneration{"Intel Skylake/Cascade Lake", []string{"t3", "c5", "m5", "r5"}} + genIntelIceLake = cpuGeneration{"Intel Ice Lake", []string{"t3", "c6i", "m6i", "r6i"}} + genIntelSapphireRapids = cpuGeneration{"Intel Sapphire Rapids", []string{"t3", "c7i", "m7i", "r7i"}} + genIntelGraniteRapids = cpuGeneration{"Intel Granite Rapids", []string{"t3", "c8i", "m8i", "r8i"}} + + // AMD generations + genAMDZen = cpuGeneration{"AMD Zen/Zen2 (Naples/Rome)", []string{"t3a", "c5a", "m5a", "r5a"}} + genAMDZen3 = cpuGeneration{"AMD Zen 3 (Milan)", []string{"t3a", "c6a", "m6a", "r6a"}} + genAMDZen4 = cpuGeneration{"AMD Zen 4 (Genoa)", []string{"t3a", "c7a", "m7a", "r7a"}} + genAMDZen5 = cpuGeneration{"AMD Zen 5 (Turin)", []string{"t3a", "c8a", "m8a", "r8a"}} + + // ARM generations + genARMNeoverseN1 = cpuGeneration{"ARM Neoverse N1 (Graviton2)", []string{"t4g", "c6g", "m6g", "r6g"}} + genARMNeoverseV1 = cpuGeneration{"ARM Neoverse V1 (Graviton3)", []string{"t4g", "c7g", "m7g", "r7g"}} + genARMNeoverseV2 = cpuGeneration{"ARM Neoverse V2 (Graviton4)", []string{"t4g", "c8g", "m8g", "r8g"}} + + // Unknown/fallback — expose only burstable family + genUnknownIntel = cpuGeneration{"Unknown Intel", []string{"t3"}} + genUnknownAMD = cpuGeneration{"Unknown AMD", []string{"t3a"}} + genUnknownARM = cpuGeneration{"Unknown ARM", []string{"t4g"}} + genUnknown = cpuGeneration{"Unknown", []string{"t3"}} +) + +// detectCPUGeneration detects the CPU microarchitecture generation using CPUID. +func detectCPUGeneration() cpuGeneration { + switch cpuid.CPU.VendorID { + case cpuid.Intel: + return detectIntelGeneration(cpuid.CPU.Family, cpuid.CPU.Model) + case cpuid.AMD: + return detectAMDGeneration(cpuid.CPU.Family, cpuid.CPU.Model) + default: + if runtime.GOARCH == "arm64" { + return detectARMGeneration() + } + slog.Warn("CPUID vendor not recognized, falling back to brand string detection", + "vendorID", cpuid.CPU.VendorID, "brand", cpuid.CPU.BrandName) } - cpuUpper := strings.ToUpper(cpuModel) - for _, entry := range cpuToVendor { - if strings.Contains(cpuUpper, strings.ToUpper(entry.pattern)) { - return entry.vendor + return detectGenerationFromBrand(cpuid.CPU.BrandName, runtime.GOARCH) +} + +// detectIntelGeneration maps Intel CPUID Family 6 model numbers to generations. +func detectIntelGeneration(family, model int) cpuGeneration { + if family != 6 { + slog.Warn("Unrecognized Intel CPU family, exposing t3 only", "family", family, "model", model) + return genUnknownIntel + } + + switch model { + case 79, 86: // Broadwell server (BDX, BDX-DE) + return genIntelBroadwell + case 85: // Skylake-SP / Cascade Lake-SP + return genIntelSkylake + case 106, 108: // Ice Lake server (ICX, ICX-D) + return genIntelIceLake + case 143, 207: // Sapphire Rapids (SPR, EMR) + return genIntelSapphireRapids + case 173, 174: // Granite Rapids (GNR, GNR-D) + return genIntelGraniteRapids + + // Consumer/desktop mapped to nearest server generation + case 151, 154: // Alder Lake + return genIntelIceLake + case 183, 191: // Raptor Lake + return genIntelSapphireRapids + case 197, 198: // Arrow Lake + return genIntelGraniteRapids + } + + slog.Warn("Unrecognized Intel CPU model, exposing t3 only", "family", family, "model", model, "brand", cpuid.CPU.BrandName) + return genUnknownIntel +} + +// detectAMDGeneration maps AMD CPUID family/model to generations. +func detectAMDGeneration(family, model int) cpuGeneration { + switch family { + case 23: // Zen, Zen+, Zen 2 (Naples, Rome, Matisse, etc.) + return genAMDZen + case 25: // Zen 3 and Zen 4 share family 25; model ranges distinguish them + // Zen 3 models: 0x00-0x0F (Milan/Vermeer), 0x20-0x5F (Rembrandt/Barcelo) + // Zen 4 models: 0x10-0x1F (Genoa), 0x60+ (Raphael/Phoenix) + isZen3 := model < 0x10 || (model >= 0x20 && model < 0x60) + if isZen3 { + return genAMDZen3 + } + return genAMDZen4 + case 26: // Zen 5 (Turin, Granite Ridge) + return genAMDZen5 + } + + slog.Warn("Unrecognized AMD CPU family, exposing t3a only", "family", family, "model", model, "brand", cpuid.CPU.BrandName) + return genUnknownAMD +} + +// detectARMGeneration detects ARM CPU generation using brand string and feature flags. +func detectARMGeneration() cpuGeneration { + brand := strings.ToLower(cpuid.CPU.BrandName) + + // Check for specific Graviton versions + if strings.Contains(brand, "graviton4") || strings.Contains(brand, "neoverse-v2") { + return genARMNeoverseV2 + } + if strings.Contains(brand, "graviton3") || strings.Contains(brand, "neoverse-v1") { + return genARMNeoverseV1 + } + if strings.Contains(brand, "graviton2") || strings.Contains(brand, "neoverse-n1") { + return genARMNeoverseN1 + } + + // SVE indicates Neoverse V1+ but cannot distinguish V1 from V2 + if cpuid.CPU.Has(cpuid.SVE) { + slog.Warn("ARM generation detected via SVE heuristic, defaulting to Neoverse V1", "brand", cpuid.CPU.BrandName) + return genARMNeoverseV1 + } + + // Default ARM to Neoverse N1 (most common) + slog.Warn("Could not identify ARM generation, defaulting to Neoverse N1", "brand", cpuid.CPU.BrandName) + return genARMNeoverseN1 +} + +// detectGenerationFromBrand is a fallback for VMs/hypervisors where CPUID may be virtualized. +func detectGenerationFromBrand(brand, arch string) cpuGeneration { + if arch == "arm64" { + return detectARMGeneration() + } + + brandLower := strings.ToLower(brand) + + // Intel patterns + if strings.Contains(brandLower, "xeon") || strings.Contains(brandLower, "intel") { + switch { + case strings.Contains(brandLower, "granite"): + return genIntelGraniteRapids + case strings.Contains(brandLower, "sapphire"): + return genIntelSapphireRapids + case strings.Contains(brandLower, "ice lake") || strings.Contains(brandLower, "icelake"): + return genIntelIceLake + case strings.Contains(brandLower, "cascade") || strings.Contains(brandLower, "skylake"): + return genIntelSkylake + case strings.Contains(brandLower, "broadwell"): + return genIntelBroadwell + default: + // Generic Intel — default to Skylake (most common in VMs) + slog.Warn("Intel CPU detected via brand string but generation unknown, defaulting to Skylake", "brand", brand) + return genIntelSkylake } } - slog.Warn("Unrecognized CPU model, defaulting to Intel vendor", "cpuModel", cpuModel) - return vendorIntel + + // AMD patterns + if strings.Contains(brandLower, "epyc") || strings.Contains(brandLower, "amd") || strings.Contains(brandLower, "ryzen") { + switch { + case strings.Contains(brandLower, "turin"): + return genAMDZen5 + case strings.Contains(brandLower, "genoa") || strings.Contains(brandLower, "9004") || strings.Contains(brandLower, "raphael"): + return genAMDZen4 + case strings.Contains(brandLower, "milan") || strings.Contains(brandLower, "7003") || strings.Contains(brandLower, "vermeer"): + return genAMDZen3 + default: + // Generic AMD — default to Zen/Zen2 + slog.Warn("AMD CPU detected via brand string but generation unknown, defaulting to Zen/Zen2", "brand", brand) + return genAMDZen + } + } + + slog.Warn("Unrecognized CPU, exposing t3 only", "brand", brand, "arch", arch) + return genUnknown } // getSystemMemory returns the total system memory in GB @@ -198,73 +347,14 @@ func getSystemMemory() (float64, error) { } } -// getCPUModel returns the CPU model name for the host system -func getCPUModel() (string, error) { - switch runtime.GOOS { - case "darwin": - // macOS: use sysctl - cmd := exec.Command("sysctl", "-n", "machdep.cpu.brand_string") - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to get CPU model on macOS: %w", err) - } - return strings.TrimSpace(string(output)), nil - - case "linux": - // Linux: read from /proc/cpuinfo - file, err := os.Open("/proc/cpuinfo") - if err != nil { - return "", fmt.Errorf("failed to open /proc/cpuinfo: %w", err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "model name") { - parts := strings.SplitN(line, ":", 2) - if len(parts) == 2 { - return strings.TrimSpace(parts[1]), nil - } - } - } - return "", fmt.Errorf("model name not found in /proc/cpuinfo") - - default: - return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) - } -} - type instanceSize struct { suffix string vcpus int memoryGB float64 } -type cpuVendor int - -const ( - vendorIntel cpuVendor = iota - vendorAMD - vendorARM -) - -func (v cpuVendor) String() string { - switch v { - case vendorIntel: - return "Intel" - case vendorAMD: - return "AMD" - case vendorARM: - return "ARM" - default: - return "Unknown" - } -} - type instanceFamilyDef struct { name string - vendor cpuVendor sizes []instanceSize currentGen bool } @@ -344,61 +434,67 @@ var memorySizesSmall = slices.Clone(memorySizes[:6]) // - Legacy (pre-gen4): a1, c1, c3, cc1, cc2, cg1, cr1, hi1, hs1, m1, m2, m3, r3, t1 var instanceFamilyDefs = []instanceFamilyDef{ // Burstable - {name: "t2", vendor: vendorIntel, sizes: burstableSizes, currentGen: false}, - {name: "t3", vendor: vendorIntel, sizes: burstableSizes, currentGen: true}, - {name: "t3a", vendor: vendorAMD, sizes: burstableSizes, currentGen: true}, - {name: "t4g", vendor: vendorARM, sizes: burstableSizes, currentGen: true}, + {name: "t2", sizes: burstableSizes, currentGen: false}, + {name: "t3", sizes: burstableSizes, currentGen: true}, + {name: "t3a", sizes: burstableSizes, currentGen: true}, + {name: "t4g", sizes: burstableSizes, currentGen: true}, // General Purpose (1:4 vCPU:memory) - {name: "m4", vendor: vendorIntel, sizes: gpSizesSmall, currentGen: false}, - {name: "m5", vendor: vendorIntel, sizes: gpSizes, currentGen: true}, - {name: "m5a", vendor: vendorAMD, sizes: gpSizes, currentGen: true}, - {name: "m6i", vendor: vendorIntel, sizes: gpSizes, currentGen: true}, - {name: "m6a", vendor: vendorAMD, sizes: gpSizes, currentGen: true}, - {name: "m6g", vendor: vendorARM, sizes: gpSizesSmall, currentGen: true}, - {name: "m7i", vendor: vendorIntel, sizes: gpSizes, currentGen: true}, - {name: "m7a", vendor: vendorAMD, sizes: gpSizes, currentGen: true}, - {name: "m7g", vendor: vendorARM, sizes: gpSizesSmall, currentGen: true}, - {name: "m8i", vendor: vendorIntel, sizes: gpSizes, currentGen: true}, - {name: "m8a", vendor: vendorAMD, sizes: gpSizes, currentGen: true}, - {name: "m8g", vendor: vendorARM, sizes: gpSizesSmall, currentGen: true}, + {name: "m4", sizes: gpSizesSmall, currentGen: false}, + {name: "m5", sizes: gpSizes, currentGen: true}, + {name: "m5a", sizes: gpSizes, currentGen: true}, + {name: "m6i", sizes: gpSizes, currentGen: true}, + {name: "m6a", sizes: gpSizes, currentGen: true}, + {name: "m6g", sizes: gpSizesSmall, currentGen: true}, + {name: "m7i", sizes: gpSizes, currentGen: true}, + {name: "m7a", sizes: gpSizes, currentGen: true}, + {name: "m7g", sizes: gpSizesSmall, currentGen: true}, + {name: "m8i", sizes: gpSizes, currentGen: true}, + {name: "m8a", sizes: gpSizes, currentGen: true}, + {name: "m8g", sizes: gpSizesSmall, currentGen: true}, // Compute Optimized (1:2 vCPU:memory) - {name: "c4", vendor: vendorIntel, sizes: computeSizesSmall, currentGen: false}, - {name: "c5", vendor: vendorIntel, sizes: computeSizes, currentGen: true}, - {name: "c5a", vendor: vendorAMD, sizes: computeSizes, currentGen: true}, - {name: "c6i", vendor: vendorIntel, sizes: computeSizes, currentGen: true}, - {name: "c6a", vendor: vendorAMD, sizes: computeSizes, currentGen: true}, - {name: "c6g", vendor: vendorARM, sizes: computeSizesSmall, currentGen: true}, - {name: "c7i", vendor: vendorIntel, sizes: computeSizes, currentGen: true}, - {name: "c7a", vendor: vendorAMD, sizes: computeSizes, currentGen: true}, - {name: "c7g", vendor: vendorARM, sizes: computeSizesSmall, currentGen: true}, - {name: "c8i", vendor: vendorIntel, sizes: computeSizes, currentGen: true}, - {name: "c8a", vendor: vendorAMD, sizes: computeSizes, currentGen: true}, - {name: "c8g", vendor: vendorARM, sizes: computeSizesSmall, currentGen: true}, + {name: "c4", sizes: computeSizesSmall, currentGen: false}, + {name: "c5", sizes: computeSizes, currentGen: true}, + {name: "c5a", sizes: computeSizes, currentGen: true}, + {name: "c6i", sizes: computeSizes, currentGen: true}, + {name: "c6a", sizes: computeSizes, currentGen: true}, + {name: "c6g", sizes: computeSizesSmall, currentGen: true}, + {name: "c7i", sizes: computeSizes, currentGen: true}, + {name: "c7a", sizes: computeSizes, currentGen: true}, + {name: "c7g", sizes: computeSizesSmall, currentGen: true}, + {name: "c8i", sizes: computeSizes, currentGen: true}, + {name: "c8a", sizes: computeSizes, currentGen: true}, + {name: "c8g", sizes: computeSizesSmall, currentGen: true}, // Memory Optimized (1:8 vCPU:memory) - {name: "r4", vendor: vendorIntel, sizes: memorySizesSmall, currentGen: false}, - {name: "r5", vendor: vendorIntel, sizes: memorySizes, currentGen: true}, - {name: "r5a", vendor: vendorAMD, sizes: memorySizes, currentGen: true}, - {name: "r6i", vendor: vendorIntel, sizes: memorySizes, currentGen: true}, - {name: "r6a", vendor: vendorAMD, sizes: memorySizes, currentGen: true}, - {name: "r6g", vendor: vendorARM, sizes: memorySizesSmall, currentGen: true}, - {name: "r7i", vendor: vendorIntel, sizes: memorySizes, currentGen: true}, - {name: "r7a", vendor: vendorAMD, sizes: memorySizes, currentGen: true}, - {name: "r7g", vendor: vendorARM, sizes: memorySizesSmall, currentGen: true}, - {name: "r8i", vendor: vendorIntel, sizes: memorySizes, currentGen: true}, - {name: "r8a", vendor: vendorAMD, sizes: memorySizes, currentGen: true}, - {name: "r8g", vendor: vendorARM, sizes: memorySizesSmall, currentGen: true}, + {name: "r4", sizes: memorySizesSmall, currentGen: false}, + {name: "r5", sizes: memorySizes, currentGen: true}, + {name: "r5a", sizes: memorySizes, currentGen: true}, + {name: "r6i", sizes: memorySizes, currentGen: true}, + {name: "r6a", sizes: memorySizes, currentGen: true}, + {name: "r6g", sizes: memorySizesSmall, currentGen: true}, + {name: "r7i", sizes: memorySizes, currentGen: true}, + {name: "r7a", sizes: memorySizes, currentGen: true}, + {name: "r7g", sizes: memorySizesSmall, currentGen: true}, + {name: "r8i", sizes: memorySizes, currentGen: true}, + {name: "r8a", sizes: memorySizes, currentGen: true}, + {name: "r8g", sizes: memorySizesSmall, currentGen: true}, } -// generateInstanceTypes creates the instance type map for the given CPU vendor. -// It generates all instance families matching the vendor across burstable, -// general purpose, compute optimized, and memory optimized categories. -func generateInstanceTypes(v cpuVendor, arch string) map[string]*ec2.InstanceTypeInfo { +// generateInstanceTypes creates the instance type map for the given CPU generation. +// It generates all instance families matching the generation's family list across +// burstable, general purpose, compute optimized, and memory optimized categories. +func generateInstanceTypes(gen cpuGeneration, arch string) map[string]*ec2.InstanceTypeInfo { + // Build a set of allowed families for fast lookup + allowed := make(map[string]bool, len(gen.families)) + for _, f := range gen.families { + allowed[f] = true + } + instanceTypes := make(map[string]*ec2.InstanceTypeInfo) for _, def := range instanceFamilyDefs { - if def.vendor != v { + if !allowed[def.name] { continue } burstable := strings.HasPrefix(def.name, "t") @@ -438,31 +534,25 @@ func NewResourceManager() *ResourceManager { totalMemGB = 8.0 // Default to 8GB if we can't get the actual memory } - // Get CPU model for instance family detection - cpuModel, err := getCPUModel() - if err != nil { - slog.Warn("Failed to get CPU model, using default", "err", err) - cpuModel = "Unknown" - } - // Determine architecture arch := "x86_64" if runtime.GOARCH == "arm64" { arch = "arm64" } - // Detect CPU vendor and generate instance types - vendor := vendorFromCPU(cpuModel, arch) - instanceTypes := generateInstanceTypes(vendor, arch) + // Detect CPU generation and generate matching instance types + gen := detectCPUGeneration() + instanceTypes := generateInstanceTypes(gen, arch) if len(instanceTypes) == 0 { slog.Error("No instance types generated, daemon will be unable to run VMs", - "vendor", vendor, "arch", arch, "cpuModel", cpuModel) + "generation", gen.name, "arch", arch) } slog.Info("System resources detected", - "vCPUs", numCPU, "memGB", totalMemGB, "cpu", cpuModel, - "vendor", vendor, "instanceTypes", len(instanceTypes), "os", runtime.GOOS) + "vCPUs", numCPU, "memGB", totalMemGB, + "generation", gen.name, "families", gen.families, + "instanceTypes", len(instanceTypes), "os", runtime.GOOS) return &ResourceManager{ availableVCPU: numCPU, @@ -1981,9 +2071,11 @@ func (d *Daemon) StartInstance(instance *vm.VM) error { return // Startup failed, LaunchInstance handles the error } - // Runtime crash — handle it (clean exits are expected from QMP shutdown) + // Handle exit: crash vs clean shutdown if waitErr != nil { d.handleInstanceCrash(instance, waitErr) + } else { + slog.Info("VM process exited cleanly", "instance", instance.ID) } }() diff --git a/hive/daemon/daemon_test.go b/hive/daemon/daemon_test.go index c44e5fd14..25c75e658 100644 --- a/hive/daemon/daemon_test.go +++ b/hive/daemon/daemon_test.go @@ -450,9 +450,9 @@ func TestGetInstanceTypeInfos(t *testing.T) { infos := rm.GetInstanceTypeInfos() require.NotEmpty(t, infos, "Should return at least one instance type") - // With comprehensive families, expect 49+ types (ARM) up to 128 (Intel) - assert.True(t, len(infos) >= 49, - "Should have at least 49 instance types, got %d", len(infos)) + // With generation-specific families, minimum is 7 (unknown/burstable-only) up to 31 (current-gen) + assert.True(t, len(infos) >= 7, + "Should have at least 7 instance types, got %d", len(infos)) // Verify structure of returned instance type info for _, info := range infos { @@ -470,35 +470,12 @@ func TestGetInstanceTypeInfos(t *testing.T) { } } -// TestCPUDetection tests CPU model detection +// TestCPUDetection tests that detectCPUGeneration returns a valid generation on the current host func TestCPUDetection(t *testing.T) { - cpuModel, err := getCPUModel() - require.NoError(t, err, "Should be able to detect CPU model") - assert.NotEmpty(t, cpuModel, "CPU model should not be empty") - - t.Logf("Detected CPU model: %s", cpuModel) - - // Test vendor mapping with correct architectures - testCases := []struct { - cpuModel string - arch string - expected cpuVendor - }{ - {"AMD EPYC 7551", "x86_64", vendorAMD}, - {"AMD Ryzen 9 7950X", "x86_64", vendorAMD}, - {"Intel Xeon E5-2686", "x86_64", vendorIntel}, - {"Apple M1", "arm64", vendorARM}, - {"AWS Graviton2", "arm64", vendorARM}, - {"Unknown CPU", "x86_64", vendorIntel}, - } - - for _, tt := range testCases { - t.Run(tt.cpuModel, func(t *testing.T) { - v := vendorFromCPU(tt.cpuModel, tt.arch) - assert.Equal(t, tt.expected, v, "Wrong vendor for %s on %s", tt.cpuModel, tt.arch) - t.Logf("CPU: %s (%s) -> Vendor: %s", tt.cpuModel, tt.arch, v) - }) - } + gen := detectCPUGeneration() + assert.NotEmpty(t, gen.name, "Generation name should not be empty") + assert.NotEmpty(t, gen.families, "Generation families should not be empty") + t.Logf("Detected CPU generation: %s, families: %v", gen.name, gen.families) } // TestGetAvailableInstanceTypeInfos_ResourceFiltering tests that instance types are filtered by available resources @@ -3232,60 +3209,138 @@ func countFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) int { return count } -func TestGenerateInstanceTypes_Intel(t *testing.T) { - types := generateInstanceTypes(vendorIntel, "x86_64") - // Intel: t2(7)+t3(7) + m4(6)+m5(8)+m6i(8)+m7i(8)+m8i(8) + c4(6)+c5(8)+c6i(8)+c7i(8)+c8i(8) + r4(6)+r5(8)+r6i(8)+r7i(8)+r8i(8) = 128 - assert.Len(t, types, 128) +// --- Generation-specific instance type tests --- - for _, prefix := range []string{"t2.", "t3.", "m7i.", "m8i.", "c5.", "c7i.", "r5.", "r7i."} { - assert.True(t, hasFamily(types, prefix), "expected Intel types to include %s family", prefix) +func TestGenerateInstanceTypes_IntelIceLake(t *testing.T) { + types := generateInstanceTypes(genIntelIceLake, "x86_64") + // t3(7) + c6i(8) + m6i(8) + r6i(8) = 31 + assert.Len(t, types, 31) + + for _, prefix := range []string{"t3.", "c6i.", "m6i.", "r6i."} { + assert.True(t, hasFamily(types, prefix), "expected Ice Lake types to include %s family", prefix) } - // Verify AMD/ARM families are NOT present + // Verify other generation families are NOT present for name := range types { - assert.False(t, strings.HasPrefix(name, "t3a."), "Intel should not have t3a: %s", name) - assert.False(t, strings.HasPrefix(name, "t4g."), "Intel should not have t4g: %s", name) - assert.False(t, strings.HasPrefix(name, "m7a."), "Intel should not have m7a: %s", name) - assert.False(t, strings.HasPrefix(name, "m7g."), "Intel should not have m7g: %s", name) + assert.False(t, strings.HasPrefix(name, "t3a."), "Ice Lake should not have t3a: %s", name) + assert.False(t, strings.HasPrefix(name, "c5."), "Ice Lake should not have c5: %s", name) + assert.False(t, strings.HasPrefix(name, "c7i."), "Ice Lake should not have c7i: %s", name) } } -func TestGenerateInstanceTypes_AMD(t *testing.T) { - types := generateInstanceTypes(vendorAMD, "x86_64") - // AMD: t3a(7) + m5a(8)+m6a(8)+m7a(8)+m8a(8) + c5a(8)+c6a(8)+c7a(8)+c8a(8) + r5a(8)+r6a(8)+r7a(8)+r8a(8) = 103 - assert.Len(t, types, 103) +func TestGenerateInstanceTypes_IntelBroadwell(t *testing.T) { + types := generateInstanceTypes(genIntelBroadwell, "x86_64") + // t2(7) + c4(6) + m4(6) + r4(6) = 25 + assert.Len(t, types, 25) - for _, prefix := range []string{"t3a.", "m5a.", "m8a.", "c5a.", "c8a.", "r5a.", "r8a."} { - assert.True(t, hasFamily(types, prefix), "expected AMD types to include %s family", prefix) - } - - // Verify Intel/ARM families are NOT present - for name := range types { - assert.False(t, strings.HasPrefix(name, "t3."), "AMD should not have t3: %s", name) - assert.False(t, strings.HasPrefix(name, "t4g."), "AMD should not have t4g: %s", name) - assert.False(t, strings.HasPrefix(name, "m7i."), "AMD should not have m7i: %s", name) + for _, prefix := range []string{"t2.", "c4.", "m4.", "r4."} { + assert.True(t, hasFamily(types, prefix), "expected Broadwell types to include %s family", prefix) } } -func TestGenerateInstanceTypes_ARM(t *testing.T) { - types := generateInstanceTypes(vendorARM, "arm64") - // ARM: t4g(7) + m6g(6)+m7g(6)+m8g(6) + c6g(6)+c7g(6)+c8g(6) + r6g(6)+r7g(6)+r8g(6) = 61 - assert.Len(t, types, 61) +func TestGenerateInstanceTypes_IntelSkylake(t *testing.T) { + types := generateInstanceTypes(genIntelSkylake, "x86_64") + // t3(7) + c5(8) + m5(8) + r5(8) = 31 + assert.Len(t, types, 31) - for _, prefix := range []string{"t4g.", "m7g.", "m8g.", "c7g.", "c8g.", "r7g.", "r8g."} { - assert.True(t, hasFamily(types, prefix), "expected ARM types to include %s family", prefix) + for _, prefix := range []string{"t3.", "c5.", "m5.", "r5."} { + assert.True(t, hasFamily(types, prefix), "expected Skylake types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_IntelSapphireRapids(t *testing.T) { + types := generateInstanceTypes(genIntelSapphireRapids, "x86_64") + // t3(7) + c7i(8) + m7i(8) + r7i(8) = 31 + assert.Len(t, types, 31) + + for _, prefix := range []string{"t3.", "c7i.", "m7i.", "r7i."} { + assert.True(t, hasFamily(types, prefix), "expected Sapphire Rapids types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_IntelGraniteRapids(t *testing.T) { + types := generateInstanceTypes(genIntelGraniteRapids, "x86_64") + // t3(7) + c8i(8) + m8i(8) + r8i(8) = 31 + assert.Len(t, types, 31) + + for _, prefix := range []string{"t3.", "c8i.", "m8i.", "r8i."} { + assert.True(t, hasFamily(types, prefix), "expected Granite Rapids types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_AMDZen(t *testing.T) { + types := generateInstanceTypes(genAMDZen, "x86_64") + // t3a(7) + c5a(8) + m5a(8) + r5a(8) = 31 + assert.Len(t, types, 31) + + for _, prefix := range []string{"t3a.", "c5a.", "m5a.", "r5a."} { + assert.True(t, hasFamily(types, prefix), "expected Zen types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_AMDZen4(t *testing.T) { + types := generateInstanceTypes(genAMDZen4, "x86_64") + // t3a(7) + c7a(8) + m7a(8) + r7a(8) = 31 + assert.Len(t, types, 31) + + for _, prefix := range []string{"t3a.", "c7a.", "m7a.", "r7a."} { + assert.True(t, hasFamily(types, prefix), "expected Zen 4 types to include %s family", prefix) + } + + // Verify older AMD families are NOT present + for name := range types { + assert.False(t, strings.HasPrefix(name, "c5a."), "Zen4 should not have c5a: %s", name) + assert.False(t, strings.HasPrefix(name, "c6a."), "Zen4 should not have c6a: %s", name) + } +} + +func TestGenerateInstanceTypes_ARMN1(t *testing.T) { + types := generateInstanceTypes(genARMNeoverseN1, "arm64") + // t4g(7) + c6g(6) + m6g(6) + r6g(6) = 25 + assert.Len(t, types, 25) + + for _, prefix := range []string{"t4g.", "c6g.", "m6g.", "r6g."} { + assert.True(t, hasFamily(types, prefix), "expected N1 types to include %s family", prefix) } // Verify Intel/AMD families are NOT present for name := range types { assert.False(t, strings.HasPrefix(name, "t3."), "ARM should not have t3: %s", name) assert.False(t, strings.HasPrefix(name, "t3a."), "ARM should not have t3a: %s", name) - assert.False(t, strings.HasPrefix(name, "m7i."), "ARM should not have m7i: %s", name) } } +func TestGenerateInstanceTypes_ARMV2(t *testing.T) { + types := generateInstanceTypes(genARMNeoverseV2, "arm64") + // t4g(7) + c8g(6) + m8g(6) + r8g(6) = 25 + assert.Len(t, types, 25) + + for _, prefix := range []string{"t4g.", "c8g.", "m8g.", "r8g."} { + assert.True(t, hasFamily(types, prefix), "expected V2 types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_UnknownFallback(t *testing.T) { + types := generateInstanceTypes(genUnknownIntel, "x86_64") + // Unknown Intel: t3 only = 7 types + assert.Len(t, types, 7) + assert.True(t, hasFamily(types, "t3."), "unknown Intel should have t3") + + types = generateInstanceTypes(genUnknownAMD, "x86_64") + assert.Len(t, types, 7) + assert.True(t, hasFamily(types, "t3a."), "unknown AMD should have t3a") + + types = generateInstanceTypes(genUnknownARM, "arm64") + assert.Len(t, types, 7) + assert.True(t, hasFamily(types, "t4g."), "unknown ARM should have t4g") + + types = generateInstanceTypes(genUnknown, "x86_64") + assert.Len(t, types, 7) + assert.True(t, hasFamily(types, "t3."), "completely unknown should have t3") +} + func TestGenerateInstanceTypes_VerifyBurstableSizes(t *testing.T) { - types := generateInstanceTypes(vendorIntel, "x86_64") + types := generateInstanceTypes(genIntelSkylake, "x86_64") expected := map[string]struct { vcpus int64 @@ -3309,51 +3364,61 @@ func TestGenerateInstanceTypes_VerifyBurstableSizes(t *testing.T) { } func TestGenerateInstanceTypes_ComputeRatio(t *testing.T) { - types := generateInstanceTypes(vendorIntel, "x86_64") - - // Compute optimized: 1:2 vCPU:memory ratio - expected := map[string]struct { + // Skylake for c5 + skylakeTypes := generateInstanceTypes(genIntelSkylake, "x86_64") + expectedSkylake := map[string]struct { vcpus int64 memMB int64 }{ - "c5.large": {2, 4096}, - "c5.xlarge": {4, 8192}, - "c5.2xlarge": {8, 16384}, - "c7i.4xlarge": {16, 32768}, + "c5.large": {2, 4096}, + "c5.xlarge": {4, 8192}, + "c5.2xlarge": {8, 16384}, } - for name, exp := range expected { - it, ok := types[name] + for name, exp := range expectedSkylake { + it, ok := skylakeTypes[name] require.True(t, ok, "missing instance type %s", name) assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name) assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name) } + + // Sapphire Rapids for c7i + sapphireTypes := generateInstanceTypes(genIntelSapphireRapids, "x86_64") + it, ok := sapphireTypes["c7i.4xlarge"] + require.True(t, ok, "missing instance type c7i.4xlarge") + assert.Equal(t, int64(16), *it.VCpuInfo.DefaultVCpus, "c7i.4xlarge vCPUs") + assert.Equal(t, int64(32768), *it.MemoryInfo.SizeInMiB, "c7i.4xlarge memory") } func TestGenerateInstanceTypes_MemoryRatio(t *testing.T) { - types := generateInstanceTypes(vendorIntel, "x86_64") - - // Memory optimized: 1:8 vCPU:memory ratio - expected := map[string]struct { + // Skylake for r5 + skylakeTypes := generateInstanceTypes(genIntelSkylake, "x86_64") + expectedSkylake := map[string]struct { vcpus int64 memMB int64 }{ - "r5.large": {2, 16384}, - "r5.xlarge": {4, 32768}, - "r5.2xlarge": {8, 65536}, - "r7i.4xlarge": {16, 131072}, + "r5.large": {2, 16384}, + "r5.xlarge": {4, 32768}, + "r5.2xlarge": {8, 65536}, } - for name, exp := range expected { - it, ok := types[name] + for name, exp := range expectedSkylake { + it, ok := skylakeTypes[name] require.True(t, ok, "missing instance type %s", name) assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name) assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name) } + + // Sapphire Rapids for r7i + sapphireTypes := generateInstanceTypes(genIntelSapphireRapids, "x86_64") + it, ok := sapphireTypes["r7i.4xlarge"] + require.True(t, ok, "missing instance type r7i.4xlarge") + assert.Equal(t, int64(16), *it.VCpuInfo.DefaultVCpus, "r7i.4xlarge vCPUs") + assert.Equal(t, int64(131072), *it.MemoryInfo.SizeInMiB, "r7i.4xlarge memory") } func TestGenerateInstanceTypes_NoSmallSizesForNonBurstable(t *testing.T) { - types := generateInstanceTypes(vendorIntel, "x86_64") + types := generateInstanceTypes(genIntelSkylake, "x86_64") // Non-burstable families should not have nano/micro/small/medium sizes for name := range types { @@ -3368,21 +3433,21 @@ func TestGenerateInstanceTypes_NoSmallSizesForNonBurstable(t *testing.T) { } func TestGenerateInstanceTypes_OlderFamiliesHaveSmallerSizeRange(t *testing.T) { - types := generateInstanceTypes(vendorIntel, "x86_64") + // Broadwell has m4 = 6 sizes + broadwellTypes := generateInstanceTypes(genIntelBroadwell, "x86_64") + assert.Equal(t, 6, countFamily(broadwellTypes, "m4."), "m4 should have 6 sizes (large → 16xlarge)") - // m4 (older Intel GP) should have 6 sizes (large → 16xlarge, no 24xlarge) - assert.Equal(t, 6, countFamily(types, "m4."), "m4 should have 6 sizes (large → 16xlarge)") - // m5 (newer Intel GP) should have 8 sizes (large → 24xlarge) - assert.Equal(t, 8, countFamily(types, "m5."), "m5 should have 8 sizes (large → 24xlarge)") + // Skylake has m5 = 8 sizes + skylakeTypes := generateInstanceTypes(genIntelSkylake, "x86_64") + assert.Equal(t, 8, countFamily(skylakeTypes, "m5."), "m5 should have 8 sizes (large → 24xlarge)") } func TestGenerateInstanceTypes_BurstableFlag(t *testing.T) { - types := generateInstanceTypes(vendorIntel, "x86_64") - - // Previous-gen families: t2, m4, c4, r4 + // Test Broadwell (has prev-gen families) + broadwellTypes := generateInstanceTypes(genIntelBroadwell, "x86_64") prevGen := map[string]bool{"t2": true, "m4": true, "c4": true, "r4": true} - for name, info := range types { + for name, info := range broadwellTypes { isBurstable := strings.HasPrefix(name, "t") family := strings.SplitN(name, ".", 2)[0] assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported, @@ -3390,30 +3455,116 @@ func TestGenerateInstanceTypes_BurstableFlag(t *testing.T) { assert.Equal(t, !prevGen[family], *info.CurrentGeneration, "%s current generation flag mismatch", name) } + + // Test current-gen (Sapphire Rapids) — all families should be currentGen=true + sapphireTypes := generateInstanceTypes(genIntelSapphireRapids, "x86_64") + for name, info := range sapphireTypes { + isBurstable := strings.HasPrefix(name, "t") + assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported, + "%s burstable flag mismatch", name) + assert.True(t, *info.CurrentGeneration, + "%s should be current generation", name) + } } -// --- vendorFromCPU --- +// --- CPU generation detection tests --- -func TestVendorFromCPU_ExactMappings(t *testing.T) { - assert.Equal(t, vendorAMD, vendorFromCPU("AMD EPYC 7551", "x86_64")) - assert.Equal(t, vendorAMD, vendorFromCPU("AMD Ryzen 9 7950X", "x86_64")) - assert.Equal(t, vendorAMD, vendorFromCPU("AMD Ryzen Threadripper PRO 5995WX", "x86_64")) - assert.Equal(t, vendorIntel, vendorFromCPU("Intel Xeon E5-2686", "x86_64")) - assert.Equal(t, vendorARM, vendorFromCPU("Apple M1", "arm64")) - assert.Equal(t, vendorARM, vendorFromCPU("AWS Graviton2", "arm64")) - assert.Equal(t, vendorARM, vendorFromCPU("ARM Cortex-A72", "arm64")) +func TestDetectIntelGeneration(t *testing.T) { + tests := []struct { + name string + family int + model int + expected cpuGeneration + }{ + {"Broadwell BDX", 6, 79, genIntelBroadwell}, + {"Broadwell BDX-DE", 6, 86, genIntelBroadwell}, + {"Skylake-SP", 6, 85, genIntelSkylake}, + {"Ice Lake ICX", 6, 106, genIntelIceLake}, + {"Ice Lake ICX-D", 6, 108, genIntelIceLake}, + {"Sapphire Rapids SPR", 6, 143, genIntelSapphireRapids}, + {"Emerald Rapids EMR", 6, 207, genIntelSapphireRapids}, + {"Granite Rapids GNR", 6, 173, genIntelGraniteRapids}, + {"Granite Rapids GNR-D", 6, 174, genIntelGraniteRapids}, + // Consumer mappings + {"Alder Lake", 6, 151, genIntelIceLake}, + {"Alder Lake P", 6, 154, genIntelIceLake}, + {"Raptor Lake", 6, 183, genIntelSapphireRapids}, + {"Raptor Lake P", 6, 191, genIntelSapphireRapids}, + {"Arrow Lake", 6, 197, genIntelGraniteRapids}, + {"Arrow Lake S", 6, 198, genIntelGraniteRapids}, + // Unknown + {"Unknown family", 15, 0, genUnknownIntel}, + {"Unknown model", 6, 255, genUnknownIntel}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gen := detectIntelGeneration(tt.family, tt.model) + assert.Equal(t, tt.expected.name, gen.name) + assert.Equal(t, tt.expected.families, gen.families) + }) + } } -func TestVendorFromCPU_CaseInsensitive(t *testing.T) { - assert.Equal(t, vendorAMD, vendorFromCPU("amd epyc 9654", "x86_64")) +func TestDetectAMDGeneration(t *testing.T) { + tests := []struct { + name string + family int + model int + expected cpuGeneration + }{ + {"Naples/Rome family 23", 23, 1, genAMDZen}, + {"Zen3 Milan model 0x01", 25, 0x01, genAMDZen3}, + {"Zen3 Vermeer model 0x21", 25, 0x21, genAMDZen3}, + {"Zen4 Genoa model 0x11", 25, 0x11, genAMDZen4}, + {"Zen4 Raphael model 0x61", 25, 0x61, genAMDZen4}, + // Boundary tests for family 25 Zen3/Zen4 split + {"Zen3 boundary 0x0F", 25, 0x0F, genAMDZen3}, + {"Zen4 boundary 0x10", 25, 0x10, genAMDZen4}, + {"Zen4 boundary 0x1F", 25, 0x1F, genAMDZen4}, + {"Zen3 boundary 0x20", 25, 0x20, genAMDZen3}, + {"Zen3 boundary 0x5F", 25, 0x5F, genAMDZen3}, + {"Zen4 boundary 0x60", 25, 0x60, genAMDZen4}, + {"Zen4 max model", 25, 0xFF, genAMDZen4}, + {"Zen5 Turin family 26", 26, 0, genAMDZen5}, + {"Unknown AMD family", 20, 0, genUnknownAMD}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gen := detectAMDGeneration(tt.family, tt.model) + assert.Equal(t, tt.expected.name, gen.name) + assert.Equal(t, tt.expected.families, gen.families) + }) + } } -func TestVendorFromCPU_DefaultFallback(t *testing.T) { - // Unknown CPU on x86_64 defaults to Intel - assert.Equal(t, vendorIntel, vendorFromCPU("Unknown CPU Model XYZ", "x86_64")) - // Any CPU on arm64 always returns ARM - assert.Equal(t, vendorARM, vendorFromCPU("Unknown CPU Model XYZ", "arm64")) - assert.Equal(t, vendorARM, vendorFromCPU("Intel Xeon E5-2686", "arm64")) +func TestDetectGenerationFromBrand(t *testing.T) { + tests := []struct { + name string + brand string + arch string + expected cpuGeneration + }{ + {"Intel Skylake brand", "Intel(R) Xeon(R) Platinum 8175M (Skylake)", "x86_64", genIntelSkylake}, + {"Intel Ice Lake brand", "Intel(R) Xeon(R) Platinum 8375C (Ice Lake)", "x86_64", genIntelIceLake}, + {"Intel Sapphire brand", "Intel(R) Xeon(R) w9-3495X (Sapphire Rapids)", "x86_64", genIntelSapphireRapids}, + {"Intel Granite brand", "Intel(R) Xeon(R) 6980P (Granite Rapids)", "x86_64", genIntelGraniteRapids}, + {"Intel Broadwell brand", "Intel(R) Xeon(R) E5-2686 v4 (Broadwell)", "x86_64", genIntelBroadwell}, + {"Generic Intel Xeon", "Intel(R) Xeon(R) CPU E5-2686 v4", "x86_64", genIntelSkylake}, // defaults to Skylake + {"AMD Milan brand", "AMD EPYC 7003 Milan", "x86_64", genAMDZen3}, + {"AMD Genoa brand", "AMD EPYC 9004 Series", "x86_64", genAMDZen4}, + {"Generic AMD EPYC", "AMD EPYC 7551", "x86_64", genAMDZen}, + {"Completely unknown", "Some Random CPU", "x86_64", genUnknown}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gen := detectGenerationFromBrand(tt.brand, tt.arch) + assert.Equal(t, tt.expected.name, gen.name) + assert.Equal(t, tt.expected.families, gen.families) + }) + } } // --- instanceTypeVCPUs / instanceTypeMemoryMiB nil safety --- diff --git a/hive/vm/vm.go b/hive/vm/vm.go index 622713e24..3f672f0c5 100644 --- a/hive/vm/vm.go +++ b/hive/vm/vm.go @@ -102,7 +102,6 @@ type IOThread struct { type Config struct { Name string `json:"name"` - Daemonize bool `json:"daemonize"` PIDFile string `json:"pid_file"` QMPSocket string `json:"qmp_socket"` EnableKVM bool `json:"enable_kvm"` @@ -128,10 +127,6 @@ func (cfg *Config) Execute() (*exec.Cmd, error) { args := []string{} - if cfg.Daemonize { - args = append(args, "-daemonize") - } - if cfg.PIDFile != "" { args = append(args, "-pidfile", cfg.PIDFile) }