feat: detect CPU generation instead of vendor for instance types (#40)

This commit is contained in:
Julian Sommer
2026-02-17 14:59:29 +11:00
committed by GitHub
parent b7396b97c0
commit 7128a22fdb
4 changed files with 494 additions and 256 deletions
+1 -1
View File
@@ -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
+232 -140
View File
@@ -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)
}
}()
+261 -110
View File
@@ -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 ---
-5
View File
@@ -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)
}