From 83b1435c999326a95b7fa8d60ecbdeba9f5966cc Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:54:45 +1100 Subject: [PATCH] fix: refactor instance type detection (#41) --- hive/daemon/daemon.go | 366 +------------------------- hive/daemon/daemon_test.go | 387 ---------------------------- hive/instancetypes/definitions.go | 171 ++++++++++++ hive/instancetypes/detect.go | 171 ++++++++++++ hive/instancetypes/detect_test.go | 239 +++++++++++++++++ hive/instancetypes/generate.go | 73 ++++++ hive/instancetypes/generate_test.go | 315 ++++++++++++++++++++++ 7 files changed, 972 insertions(+), 750 deletions(-) create mode 100644 hive/instancetypes/definitions.go create mode 100644 hive/instancetypes/detect.go create mode 100644 hive/instancetypes/detect_test.go create mode 100644 hive/instancetypes/generate.go create mode 100644 hive/instancetypes/generate_test.go diff --git a/hive/daemon/daemon.go b/hive/daemon/daemon.go index de85c0e47..2fd7083a5 100644 --- a/hive/daemon/daemon.go +++ b/hive/daemon/daemon.go @@ -16,7 +16,6 @@ import ( "path/filepath" "regexp" "runtime" - "slices" "strconv" "strings" "sync" @@ -27,7 +26,6 @@ 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" @@ -38,6 +36,7 @@ import ( handlers_ec2_snapshot "github.com/mulgadc/hive/hive/handlers/ec2/snapshot" handlers_ec2_tags "github.com/mulgadc/hive/hive/handlers/ec2/tags" handlers_ec2_volume "github.com/mulgadc/hive/hive/handlers/ec2/volume" + "github.com/mulgadc/hive/hive/instancetypes" "github.com/mulgadc/hive/hive/objectstore" "github.com/mulgadc/hive/hive/qmp" "github.com/mulgadc/hive/hive/utils" @@ -127,183 +126,6 @@ type Daemon struct { mu sync.Mutex } -// 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"] -} - -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) - } - 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 - } - } - - // 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 func getSystemMemory() (float64, error) { switch runtime.GOOS { @@ -347,181 +169,6 @@ func getSystemMemory() (float64, error) { } } -type instanceSize struct { - suffix string - vcpus int - memoryGB float64 -} - -type instanceFamilyDef struct { - name string - sizes []instanceSize - currentGen bool -} - -// Size tables for each instance category - -var burstableSizes = []instanceSize{ - {"nano", 2, 0.5}, - {"micro", 2, 1}, - {"small", 2, 2}, - {"medium", 2, 4}, - {"large", 2, 8}, - {"xlarge", 4, 16}, - {"2xlarge", 8, 32}, -} - -var gpSizes = []instanceSize{ - {"large", 2, 8}, - {"xlarge", 4, 16}, - {"2xlarge", 8, 32}, - {"4xlarge", 16, 64}, - {"8xlarge", 32, 128}, - {"12xlarge", 48, 192}, - {"16xlarge", 64, 256}, - {"24xlarge", 96, 384}, -} - -// gpSizesSmall is gpSizes without 12xlarge and 24xlarge (older/ARM families). -var gpSizesSmall = slices.Clone(gpSizes[:6]) - -var computeSizes = []instanceSize{ - {"large", 2, 4}, - {"xlarge", 4, 8}, - {"2xlarge", 8, 16}, - {"4xlarge", 16, 32}, - {"8xlarge", 32, 64}, - {"12xlarge", 48, 96}, - {"16xlarge", 64, 128}, - {"24xlarge", 96, 192}, -} - -// computeSizesSmall is computeSizes without 12xlarge and 24xlarge (older/ARM families). -var computeSizesSmall = slices.Clone(computeSizes[:6]) - -var memorySizes = []instanceSize{ - {"large", 2, 16}, - {"xlarge", 4, 32}, - {"2xlarge", 8, 64}, - {"4xlarge", 16, 128}, - {"8xlarge", 32, 256}, - {"12xlarge", 48, 384}, - {"16xlarge", 64, 512}, - {"24xlarge", 96, 768}, -} - -// memorySizesSmall is memorySizes without 12xlarge and 24xlarge (older/ARM families). -var memorySizesSmall = slices.Clone(memorySizes[:6]) - -// instanceFamilyDefs defines all supported instance families with their vendor and sizes. -// -// We support the core families across burstable, general purpose, compute optimized, -// and memory optimized categories. The following AWS family categories are intentionally -// excluded because they require specialized hardware not available on standard bare-metal hosts: -// -// - Local disk variants (d/n suffixes): c5d, c5ad, c5n, m5d, m5ad, m5n, m5dn, m5zn, r5d, r5ad, -// r5n, r5dn, r5b, c6gd, c6gn, c6id, c6in, m6gd, m6id, m6idn, m6in, r6gd, r6id, r6idn, r6in, -// c7gd, c7gn, c7i-flex, m7gd, m7i-flex, r7gd, r7iz, c8gd, c8gn, c8i-flex, m8gd, m8i-flex, -// r8gd, r8gn, r8gb, r8i-flex — require NVMe instance storage or enhanced networking -// - GPU/accelerator: g2-g6, g6e, g6f, gr6, gr6f, p2-p6, inf1-inf2, trn1-trn2, dl1, dl2q — require -// GPU, Inferentia, Trainium, or other accelerator hardware. note inf1 and trn1 are not supported since its aws only hardware. -// - Storage optimized: d2, d3, d3en, h1, i2-i8g, i7ie, i8ge, im4gn, is4gen — require dense HDD/NVMe -// - FPGA: f1, f2 — require FPGA hardware -// - High memory: u-*, u7i-*, x1, x1e, x2gd, x2idn, x2iedn, x2iezn, x8g — require TB-scale memory -// - High frequency: z1d — specialized high clock-speed instances -// - (unsupported) Dedicated host: mac*, hpc* — require macOS/Apple hardware or HPC interconnects -// - (unsupported) Video: vt1 — requires video transcoding hardware -// - Legacy (pre-gen4): a1, c1, c3, cc1, cc2, cg1, cr1, hi1, hs1, m1, m2, m3, r3, t1 -var instanceFamilyDefs = []instanceFamilyDef{ - // Burstable - {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", 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", 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", 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 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 !allowed[def.name] { - continue - } - burstable := strings.HasPrefix(def.name, "t") - for _, size := range def.sizes { - name := fmt.Sprintf("%s.%s", def.name, size.suffix) - instanceTypes[name] = &ec2.InstanceTypeInfo{ - InstanceType: aws.String(name), - VCpuInfo: &ec2.VCpuInfo{ - DefaultVCpus: aws.Int64(int64(size.vcpus)), - }, - MemoryInfo: &ec2.MemoryInfo{ - SizeInMiB: aws.Int64(int64(size.memoryGB * 1024)), - }, - ProcessorInfo: &ec2.ProcessorInfo{ - SupportedArchitectures: []*string{aws.String(arch)}, - }, - CurrentGeneration: aws.Bool(def.currentGen), - BurstablePerformanceSupported: aws.Bool(burstable), - Hypervisor: aws.String("kvm"), - SupportedVirtualizationTypes: []*string{aws.String("hvm")}, - SupportedRootDeviceTypes: []*string{aws.String("ebs")}, - } - } - } - return instanceTypes -} - // NewResourceManager creates a new resource manager with system capabilities func NewResourceManager() *ResourceManager { // Get system CPU cores @@ -541,18 +188,11 @@ func NewResourceManager() *ResourceManager { } // 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", - "generation", gen.name, "arch", arch) - } + instanceTypes := instancetypes.DetectAndGenerate(instancetypes.HostCPU{}, arch) slog.Info("System resources detected", "vCPUs", numCPU, "memGB", totalMemGB, - "generation", gen.name, "families", gen.families, - "instanceTypes", len(instanceTypes), "os", runtime.GOOS) + "instanceTypes", len(instanceTypes)) return &ResourceManager{ availableVCPU: numCPU, diff --git a/hive/daemon/daemon_test.go b/hive/daemon/daemon_test.go index 25c75e658..18316d8e6 100644 --- a/hive/daemon/daemon_test.go +++ b/hive/daemon/daemon_test.go @@ -470,14 +470,6 @@ func TestGetInstanceTypeInfos(t *testing.T) { } } -// TestCPUDetection tests that detectCPUGeneration returns a valid generation on the current host -func TestCPUDetection(t *testing.T) { - 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 func TestGetAvailableInstanceTypeInfos_ResourceFiltering(t *testing.T) { rm := NewResourceManager() @@ -3188,385 +3180,6 @@ func TestSaveClusterConfig_ErrorOnInvalidPath(t *testing.T) { assert.Contains(t, err.Error(), "failed to write config") } -// --- generateInstanceTypes --- - -func hasFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) bool { - for name := range types { - if strings.HasPrefix(name, prefix) { - return true - } - } - return false -} - -func countFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) int { - count := 0 - for name := range types { - if strings.HasPrefix(name, prefix) { - count++ - } - } - return count -} - -// --- Generation-specific instance type tests --- - -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 other generation families are NOT present - for name := range types { - 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_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{"t2.", "c4.", "m4.", "r4."} { - assert.True(t, hasFamily(types, prefix), "expected Broadwell types to include %s family", prefix) - } -} - -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{"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) - } -} - -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(genIntelSkylake, "x86_64") - - expected := map[string]struct { - vcpus int64 - memMB int64 - }{ - "t3.nano": {2, 512}, - "t3.micro": {2, 1024}, - "t3.small": {2, 2048}, - "t3.medium": {2, 4096}, - "t3.large": {2, 8192}, - "t3.xlarge": {4, 16384}, - "t3.2xlarge": {8, 32768}, - } - - for name, exp := range expected { - it, ok := types[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) - } -} - -func TestGenerateInstanceTypes_ComputeRatio(t *testing.T) { - // 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}, - } - - 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) { - // 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}, - } - - 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(genIntelSkylake, "x86_64") - - // Non-burstable families should not have nano/micro/small/medium sizes - for name := range types { - if strings.HasPrefix(name, "t") { - continue // skip all burstable families - } - for _, small := range []string{".nano", ".micro", ".small", ".medium"} { - assert.False(t, strings.HasSuffix(name, small), - "non-burstable type %s should not have %s size", name, small) - } - } -} - -func TestGenerateInstanceTypes_OlderFamiliesHaveSmallerSizeRange(t *testing.T) { - // Broadwell has m4 = 6 sizes - broadwellTypes := generateInstanceTypes(genIntelBroadwell, "x86_64") - assert.Equal(t, 6, countFamily(broadwellTypes, "m4."), "m4 should have 6 sizes (large → 16xlarge)") - - // 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) { - // 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 broadwellTypes { - isBurstable := strings.HasPrefix(name, "t") - family := strings.SplitN(name, ".", 2)[0] - assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported, - "%s burstable flag mismatch", name) - 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) - } -} - -// --- CPU generation detection tests --- - -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 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 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 --- func TestInstanceTypeVCPUs_NilSafety(t *testing.T) { diff --git a/hive/instancetypes/definitions.go b/hive/instancetypes/definitions.go new file mode 100644 index 000000000..b70e88dcd --- /dev/null +++ b/hive/instancetypes/definitions.go @@ -0,0 +1,171 @@ +package instancetypes + +import "slices" + +// 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"] +} + +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"}} +) + +type instanceSize struct { + suffix string + vcpus int + memoryGB float64 +} + +type instanceFamilyDef struct { + name string + sizes []instanceSize + currentGen bool +} + +// Size tables for each instance category + +var burstableSizes = []instanceSize{ + {"nano", 2, 0.5}, + {"micro", 2, 1}, + {"small", 2, 2}, + {"medium", 2, 4}, + {"large", 2, 8}, + {"xlarge", 4, 16}, + {"2xlarge", 8, 32}, +} + +var gpSizes = []instanceSize{ + {"large", 2, 8}, + {"xlarge", 4, 16}, + {"2xlarge", 8, 32}, + {"4xlarge", 16, 64}, + {"8xlarge", 32, 128}, + {"12xlarge", 48, 192}, + {"16xlarge", 64, 256}, + {"24xlarge", 96, 384}, +} + +// gpSizesSmall is gpSizes without 12xlarge and 24xlarge (older/ARM families). +var gpSizesSmall = slices.Clone(gpSizes[:6]) + +var computeSizes = []instanceSize{ + {"large", 2, 4}, + {"xlarge", 4, 8}, + {"2xlarge", 8, 16}, + {"4xlarge", 16, 32}, + {"8xlarge", 32, 64}, + {"12xlarge", 48, 96}, + {"16xlarge", 64, 128}, + {"24xlarge", 96, 192}, +} + +// computeSizesSmall is computeSizes without 12xlarge and 24xlarge (older/ARM families). +var computeSizesSmall = slices.Clone(computeSizes[:6]) + +var memorySizes = []instanceSize{ + {"large", 2, 16}, + {"xlarge", 4, 32}, + {"2xlarge", 8, 64}, + {"4xlarge", 16, 128}, + {"8xlarge", 32, 256}, + {"12xlarge", 48, 384}, + {"16xlarge", 64, 512}, + {"24xlarge", 96, 768}, +} + +// memorySizesSmall is memorySizes without 12xlarge and 24xlarge (older/ARM families). +var memorySizesSmall = slices.Clone(memorySizes[:6]) + +// instanceFamilyDefs defines all supported instance families with their vendor and sizes. +// +// We support the core families across burstable, general purpose, compute optimized, +// and memory optimized categories. The following AWS family categories are intentionally +// excluded because they require specialized hardware not available on standard bare-metal hosts: +// +// - Local disk variants (d/n suffixes): c5d, c5ad, c5n, m5d, m5ad, m5n, m5dn, m5zn, r5d, r5ad, +// r5n, r5dn, r5b, c6gd, c6gn, c6id, c6in, m6gd, m6id, m6idn, m6in, r6gd, r6id, r6idn, r6in, +// c7gd, c7gn, c7i-flex, m7gd, m7i-flex, r7gd, r7iz, c8gd, c8gn, c8i-flex, m8gd, m8i-flex, +// r8gd, r8gn, r8gb, r8i-flex — require NVMe instance storage or enhanced networking +// - GPU/accelerator: g2-g6, g6e, g6f, gr6, gr6f, p2-p6, inf1-inf2, trn1-trn2, dl1, dl2q — require +// GPU, Inferentia, Trainium, or other accelerator hardware. note inf1 and trn1 are not supported since its aws only hardware. +// - Storage optimized: d2, d3, d3en, h1, i2-i8g, i7ie, i8ge, im4gn, is4gen — require dense HDD/NVMe +// - FPGA: f1, f2 — require FPGA hardware +// - High memory: u-*, u7i-*, x1, x1e, x2gd, x2idn, x2iedn, x2iezn, x8g — require TB-scale memory +// - High frequency: z1d — specialized high clock-speed instances +// - (unsupported) Dedicated host: mac*, hpc* — require macOS/Apple hardware or HPC interconnects +// - (unsupported) Video: vt1 — requires video transcoding hardware +// - Legacy (pre-gen4): a1, c1, c3, cc1, cc2, cg1, cr1, hi1, hs1, m1, m2, m3, r3, t1 +var instanceFamilyDefs = []instanceFamilyDef{ + // Burstable + {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", 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", 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", 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}, +} diff --git a/hive/instancetypes/detect.go b/hive/instancetypes/detect.go new file mode 100644 index 000000000..7ed1aaf61 --- /dev/null +++ b/hive/instancetypes/detect.go @@ -0,0 +1,171 @@ +package instancetypes + +import ( + "log/slog" + "strings" + + cpuid "github.com/klauspost/cpuid/v2" +) + +// CPUInfo abstracts CPU identification for testability. +type CPUInfo interface { + VendorID() cpuid.Vendor + Family() int + Model() int + BrandName() string + HasFeature(cpuid.FeatureID) bool +} + +// HostCPU implements CPUInfo by reading the host's cpuid.CPU global. +type HostCPU struct{} + +func (HostCPU) VendorID() cpuid.Vendor { return cpuid.CPU.VendorID } +func (HostCPU) Family() int { return cpuid.CPU.Family } +func (HostCPU) Model() int { return cpuid.CPU.Model } +func (HostCPU) BrandName() string { return cpuid.CPU.BrandName } +func (HostCPU) HasFeature(f cpuid.FeatureID) bool { return cpuid.CPU.Has(f) } + +// detectCPUGeneration detects the CPU microarchitecture generation using CPUID. +func detectCPUGeneration(cpu CPUInfo, arch string) cpuGeneration { + switch cpu.VendorID() { + case cpuid.Intel: + return detectIntelGeneration(cpu.Family(), cpu.Model()) + case cpuid.AMD: + return detectAMDGeneration(cpu.Family(), cpu.Model()) + default: + if arch == "arm64" { + return detectARMGeneration(cpu) + } + slog.Warn("CPUID vendor not recognized, falling back to brand string detection", + "vendorID", cpu.VendorID(), "brand", cpu.BrandName()) + } + return detectGenerationFromBrand(cpu, arch) +} + +// 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) + 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) + return genUnknownAMD +} + +// detectARMGeneration detects ARM CPU generation using brand string and feature flags. +func detectARMGeneration(cpu CPUInfo) cpuGeneration { + brand := strings.ToLower(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 cpu.HasFeature(cpuid.SVE) { + slog.Warn("ARM generation detected via SVE heuristic, defaulting to Neoverse V1", "brand", cpu.BrandName()) + return genARMNeoverseV1 + } + + // Unknown ARM — expose only burstable t4g (same as unknown Intel/AMD behavior) + slog.Warn("Could not identify ARM generation, exposing t4g only", "brand", cpu.BrandName()) + return genUnknownARM +} + +// detectGenerationFromBrand is a fallback for VMs/hypervisors where CPUID may be virtualized. +func detectGenerationFromBrand(cpu CPUInfo, arch string) cpuGeneration { + if arch == "arm64" { + return detectARMGeneration(cpu) + } + + brand := cpu.BrandName() + 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 + } + } + + // 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 +} diff --git a/hive/instancetypes/detect_test.go b/hive/instancetypes/detect_test.go new file mode 100644 index 000000000..0ca37eaaa --- /dev/null +++ b/hive/instancetypes/detect_test.go @@ -0,0 +1,239 @@ +package instancetypes + +import ( + "runtime" + "testing" + + cpuid "github.com/klauspost/cpuid/v2" + "github.com/stretchr/testify/assert" +) + +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 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 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}, + // ARM delegation path + {"ARM via brand fallback", "AWS Graviton4", "arm64", genARMNeoverseV2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockCPU{brandName: tt.brand} + gen := detectGenerationFromBrand(mock, tt.arch) + assert.Equal(t, tt.expected.name, gen.name) + assert.Equal(t, tt.expected.families, gen.families) + }) + } +} + +func TestCPUDetection(t *testing.T) { + cpu := HostCPU{} + gen := detectCPUGeneration(cpu, hostArch()) + 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) +} + +// --- Tests using mock CPUInfo --- + +type mockCPU struct { + vendorID cpuid.Vendor + family int + model int + brandName string + features map[cpuid.FeatureID]bool +} + +func (m *mockCPU) VendorID() cpuid.Vendor { return m.vendorID } +func (m *mockCPU) Family() int { return m.family } +func (m *mockCPU) Model() int { return m.model } +func (m *mockCPU) BrandName() string { return m.brandName } +func (m *mockCPU) HasFeature(f cpuid.FeatureID) bool { return m.features[f] } + +func TestDetectCPUGeneration_IntelViaInterface(t *testing.T) { + cpu := &mockCPU{ + vendorID: cpuid.Intel, + family: 6, + model: 143, // Sapphire Rapids + } + gen := detectCPUGeneration(cpu, "x86_64") + assert.Equal(t, genIntelSapphireRapids.name, gen.name) + assert.Equal(t, genIntelSapphireRapids.families, gen.families) +} + +func TestDetectCPUGeneration_AMDViaInterface(t *testing.T) { + cpu := &mockCPU{ + vendorID: cpuid.AMD, + family: 25, + model: 0x11, // Genoa (Zen 4) + } + gen := detectCPUGeneration(cpu, "x86_64") + assert.Equal(t, genAMDZen4.name, gen.name) + assert.Equal(t, genAMDZen4.families, gen.families) +} + +func TestDetectCPUGeneration_ARMViaInterface(t *testing.T) { + cpu := &mockCPU{ + vendorID: 0, // unknown vendor + brandName: "AWS Graviton3", + features: map[cpuid.FeatureID]bool{cpuid.SVE: true}, + } + gen := detectCPUGeneration(cpu, "arm64") + assert.Equal(t, genARMNeoverseV1.name, gen.name) + assert.Equal(t, genARMNeoverseV1.families, gen.families) +} + +func TestDetectCPUGeneration_ARMUnknown(t *testing.T) { + cpu := &mockCPU{ + vendorID: 0, // unknown vendor + brandName: "Some Unknown ARM Processor", + } + gen := detectCPUGeneration(cpu, "arm64") + assert.Equal(t, genUnknownARM.name, gen.name) + assert.Equal(t, genUnknownARM.families, gen.families) +} + +func TestDetectCPUGeneration_FallbackToBrand(t *testing.T) { + cpu := &mockCPU{ + vendorID: 0, // unknown vendor + brandName: "AMD EPYC 9004 Series", + } + gen := detectCPUGeneration(cpu, "x86_64") + assert.Equal(t, genAMDZen4.name, gen.name) + assert.Equal(t, genAMDZen4.families, gen.families) +} + +// --- detectARMGeneration coverage for all 5 code paths --- + +func TestDetectARMGeneration(t *testing.T) { + tests := []struct { + name string + brand string + features map[cpuid.FeatureID]bool + expected cpuGeneration + }{ + {"Graviton4 brand", "AWS Graviton4", nil, genARMNeoverseV2}, + {"Neoverse V2 brand", "ARM Neoverse-V2", nil, genARMNeoverseV2}, + {"Graviton3 brand", "AWS Graviton3", nil, genARMNeoverseV1}, + {"Neoverse V1 brand", "ARM Neoverse-V1", nil, genARMNeoverseV1}, + {"Graviton2 brand", "AWS Graviton2", nil, genARMNeoverseN1}, + {"Neoverse N1 brand", "ARM Neoverse-N1", nil, genARMNeoverseN1}, + {"SVE only, no brand match", "Generic ARM Processor", map[cpuid.FeatureID]bool{cpuid.SVE: true}, genARMNeoverseV1}, + {"Unknown ARM, no SVE", "Generic ARM Processor", nil, genUnknownARM}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cpu := &mockCPU{brandName: tt.brand, features: tt.features} + gen := detectARMGeneration(cpu) + assert.Equal(t, tt.expected.name, gen.name) + assert.Equal(t, tt.expected.families, gen.families) + }) + } +} + +// --- DetectAndGenerate (public API) --- + +func TestDetectAndGenerate_Intel(t *testing.T) { + cpu := &mockCPU{vendorID: cpuid.Intel, family: 6, model: 143} + types := DetectAndGenerate(cpu, "x86_64") + assert.NotEmpty(t, types) + assert.True(t, hasFamily(types, "c7i."), "Sapphire Rapids should include c7i") +} + +func TestDetectAndGenerate_NormalizesAmd64(t *testing.T) { + cpu := &mockCPU{vendorID: cpuid.Intel, family: 6, model: 106} + types := DetectAndGenerate(cpu, "amd64") + assert.NotEmpty(t, types) + assert.True(t, hasFamily(types, "c6i."), "amd64 should normalize to x86_64 and produce Ice Lake types") +} + +func hostArch() string { + if runtime.GOARCH == "arm64" { + return "arm64" + } + return "x86_64" +} diff --git a/hive/instancetypes/generate.go b/hive/instancetypes/generate.go new file mode 100644 index 000000000..8d5ff39c5 --- /dev/null +++ b/hive/instancetypes/generate.go @@ -0,0 +1,73 @@ +package instancetypes + +import ( + "fmt" + "log/slog" + "runtime" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" +) + +// generateForGeneration 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 generateForGeneration(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 !allowed[def.name] { + continue + } + burstable := strings.HasPrefix(def.name, "t") + for _, size := range def.sizes { + name := fmt.Sprintf("%s.%s", def.name, size.suffix) + instanceTypes[name] = &ec2.InstanceTypeInfo{ + InstanceType: aws.String(name), + VCpuInfo: &ec2.VCpuInfo{ + DefaultVCpus: aws.Int64(int64(size.vcpus)), + }, + MemoryInfo: &ec2.MemoryInfo{ + SizeInMiB: aws.Int64(int64(size.memoryGB * 1024)), + }, + ProcessorInfo: &ec2.ProcessorInfo{ + SupportedArchitectures: []*string{aws.String(arch)}, + }, + CurrentGeneration: aws.Bool(def.currentGen), + BurstablePerformanceSupported: aws.Bool(burstable), + Hypervisor: aws.String("kvm"), + SupportedVirtualizationTypes: []*string{aws.String("hvm")}, + SupportedRootDeviceTypes: []*string{aws.String("ebs")}, + } + } + } + return instanceTypes +} + +// DetectAndGenerate detects the host CPU generation and generates matching instance types. +func DetectAndGenerate(cpu CPUInfo, arch string) map[string]*ec2.InstanceTypeInfo { + // Normalize Go's "amd64" to the Linux/AWS convention "x86_64". + if arch == "amd64" { + arch = "x86_64" + } + + gen := detectCPUGeneration(cpu, arch) + types := generateForGeneration(gen, arch) + + if len(types) == 0 { + slog.Error("No instance types generated, daemon will be unable to run VMs", + "generation", gen.name, "arch", arch) + } else { + slog.Info("CPU generation detected", + "generation", gen.name, "families", gen.families, + "instanceTypes", len(types), "os", runtime.GOOS) + } + + return types +} diff --git a/hive/instancetypes/generate_test.go b/hive/instancetypes/generate_test.go new file mode 100644 index 000000000..9e9e22396 --- /dev/null +++ b/hive/instancetypes/generate_test.go @@ -0,0 +1,315 @@ +package instancetypes + +import ( + "strings" + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func hasFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) bool { + for name := range types { + if strings.HasPrefix(name, prefix) { + return true + } + } + return false +} + +func countFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) int { + count := 0 + for name := range types { + if strings.HasPrefix(name, prefix) { + count++ + } + } + return count +} + +func TestGenerateInstanceTypes_IntelIceLake(t *testing.T) { + types := generateForGeneration(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 other generation families are NOT present + for name := range types { + 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_IntelBroadwell(t *testing.T) { + types := generateForGeneration(genIntelBroadwell, "x86_64") + // t2(7) + c4(6) + m4(6) + r4(6) = 25 + assert.Len(t, types, 25) + + for _, prefix := range []string{"t2.", "c4.", "m4.", "r4."} { + assert.True(t, hasFamily(types, prefix), "expected Broadwell types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_IntelSkylake(t *testing.T) { + types := generateForGeneration(genIntelSkylake, "x86_64") + // t3(7) + c5(8) + m5(8) + r5(8) = 31 + assert.Len(t, types, 31) + + 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 := generateForGeneration(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 := generateForGeneration(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 := generateForGeneration(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 := generateForGeneration(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_AMDZen3(t *testing.T) { + types := generateForGeneration(genAMDZen3, "x86_64") + // t3a(7) + c6a(8) + m6a(8) + r6a(8) = 31 + assert.Len(t, types, 31) + + for _, prefix := range []string{"t3a.", "c6a.", "m6a.", "r6a."} { + assert.True(t, hasFamily(types, prefix), "expected Zen 3 types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_AMDZen5(t *testing.T) { + types := generateForGeneration(genAMDZen5, "x86_64") + // t3a(7) + c8a(8) + m8a(8) + r8a(8) = 31 + assert.Len(t, types, 31) + + for _, prefix := range []string{"t3a.", "c8a.", "m8a.", "r8a."} { + assert.True(t, hasFamily(types, prefix), "expected Zen 5 types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_ARMV1(t *testing.T) { + types := generateForGeneration(genARMNeoverseV1, "arm64") + // t4g(7) + c7g(6) + m7g(6) + r7g(6) = 25 + assert.Len(t, types, 25) + + for _, prefix := range []string{"t4g.", "c7g.", "m7g.", "r7g."} { + assert.True(t, hasFamily(types, prefix), "expected V1 types to include %s family", prefix) + } +} + +func TestGenerateInstanceTypes_ARMN1(t *testing.T) { + types := generateForGeneration(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) + } +} + +func TestGenerateInstanceTypes_ARMV2(t *testing.T) { + types := generateForGeneration(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 := generateForGeneration(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 = generateForGeneration(genUnknownAMD, "x86_64") + assert.Len(t, types, 7) + assert.True(t, hasFamily(types, "t3a."), "unknown AMD should have t3a") + + types = generateForGeneration(genUnknownARM, "arm64") + assert.Len(t, types, 7) + assert.True(t, hasFamily(types, "t4g."), "unknown ARM should have t4g") + + types = generateForGeneration(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 := generateForGeneration(genIntelSkylake, "x86_64") + + expected := map[string]struct { + vcpus int64 + memMB int64 + }{ + "t3.nano": {2, 512}, + "t3.micro": {2, 1024}, + "t3.small": {2, 2048}, + "t3.medium": {2, 4096}, + "t3.large": {2, 8192}, + "t3.xlarge": {4, 16384}, + "t3.2xlarge": {8, 32768}, + } + + for name, exp := range expected { + it, ok := types[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) + } +} + +func TestGenerateInstanceTypes_ComputeRatio(t *testing.T) { + // Skylake for c5 + skylakeTypes := generateForGeneration(genIntelSkylake, "x86_64") + expectedSkylake := map[string]struct { + vcpus int64 + memMB int64 + }{ + "c5.large": {2, 4096}, + "c5.xlarge": {4, 8192}, + "c5.2xlarge": {8, 16384}, + } + + 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 := generateForGeneration(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) { + // Skylake for r5 + skylakeTypes := generateForGeneration(genIntelSkylake, "x86_64") + expectedSkylake := map[string]struct { + vcpus int64 + memMB int64 + }{ + "r5.large": {2, 16384}, + "r5.xlarge": {4, 32768}, + "r5.2xlarge": {8, 65536}, + } + + 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 := generateForGeneration(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 := generateForGeneration(genIntelSkylake, "x86_64") + + // Non-burstable families should not have nano/micro/small/medium sizes + for name := range types { + if strings.HasPrefix(name, "t") { + continue // skip all burstable families + } + for _, small := range []string{".nano", ".micro", ".small", ".medium"} { + assert.False(t, strings.HasSuffix(name, small), + "non-burstable type %s should not have %s size", name, small) + } + } +} + +func TestGenerateInstanceTypes_OlderFamiliesHaveSmallerSizeRange(t *testing.T) { + // Broadwell has m4 = 6 sizes + broadwellTypes := generateForGeneration(genIntelBroadwell, "x86_64") + assert.Equal(t, 6, countFamily(broadwellTypes, "m4."), "m4 should have 6 sizes (large → 16xlarge)") + + // Skylake has m5 = 8 sizes + skylakeTypes := generateForGeneration(genIntelSkylake, "x86_64") + assert.Equal(t, 8, countFamily(skylakeTypes, "m5."), "m5 should have 8 sizes (large → 24xlarge)") +} + +func TestGenerateInstanceTypes_BurstableFlag(t *testing.T) { + // Test Broadwell (has prev-gen families) + broadwellTypes := generateForGeneration(genIntelBroadwell, "x86_64") + prevGen := map[string]bool{"t2": true, "m4": true, "c4": true, "r4": true} + + for name, info := range broadwellTypes { + isBurstable := strings.HasPrefix(name, "t") + family := strings.SplitN(name, ".", 2)[0] + assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported, + "%s burstable flag mismatch", name) + 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 := generateForGeneration(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) + } +}