diff --git a/cmd/myf5/main.go b/cmd/myf5/main.go index b4bed06..c816dbc 100644 --- a/cmd/myf5/main.go +++ b/cmd/myf5/main.go @@ -1648,6 +1648,20 @@ func handleCaseCreate(ctx context.Context, client *cases.Client, args []string) payload["preferredContactMethod"] = "Email" } + // Warn if BIG-IP TMOS or hardware platform is EOL or approaching EOL + family, _ := payload["productFamily"].(string) + ver, _ := payload["version"].(string) + prod, _ := payload["product"].(string) + if strings.EqualFold(family, "BIG-IP") || strings.Contains(strings.ToUpper(prod), "BIG-IP") { + lc := bigip.EvaluateLifecycle(ver, prod, time.Now()) + for _, w := range lc.Warnings { + fmt.Fprintf(os.Stderr, "[WARNING] %s\n", w) + } + if lc.Software.Recommendation != "" && lc.Software.Warning != "" { + fmt.Fprintf(os.Stderr, " Software Rec: %s\n", lc.Software.Recommendation) + } + } + if dryRun { out, err := json.MarshalIndent(payload, "", " ") if err != nil { diff --git a/cmd/myf5/main_test.go b/cmd/myf5/main_test.go index dc01cf4..ab2b764 100644 --- a/cmd/myf5/main_test.go +++ b/cmd/myf5/main_test.go @@ -5,8 +5,10 @@ import ( "bytes" "compress/gzip" "context" + "io" "os" "path/filepath" + "strings" "testing" "myf5-cli/pkg/cases" @@ -208,3 +210,48 @@ func TestCaseCreate_NoArgsNonTTY(t *testing.T) { t.Errorf("expected exit code 1 for case create without args, got code=%d exited=%v", code, exited) } } + +func TestCaseCreate_LifecycleWarnings(t *testing.T) { + origStderr := os.Stderr + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("failed to create pipe: %v", err) + } + os.Stderr = w + + args := []string{ + "--dry-run", + "--family", "BIG-IP", + "--product", "BIG-IP 2000s", + "--version", "15.1.8", + "--serial", "test-serial-12345", + "--priority", "4", + "--reason", "General Problem", + "--subject", "Testing EOL", + "--description", "EOL description", + "--email", "user@example.com", + "--timezone", "(GMT -05:00) EST", + } + + handleCaseCreate(context.Background(), nil, args) + + w.Close() + os.Stderr = origStderr + + var buf bytes.Buffer + io.Copy(&buf, r) + errOutput := buf.String() + + if !strings.Contains(errOutput, "[WARNING] TMOS version 15.1.8 reached End of Life") { + t.Errorf("expected TMOS EOL warning in stderr, got: %q", errOutput) + } + if !strings.Contains(errOutput, "[WARNING] Hardware platform BIG-IP 2000s reached End of Life") { + t.Errorf("expected Hardware EOL warning in stderr, got: %q", errOutput) + } + if !strings.Contains(errOutput, "Software Rec:") { + t.Errorf("expected Software Rec in stderr, got: %q", errOutput) + } + if strings.Contains(errOutput, "Hardware Rec:") { + t.Errorf("expected no Hardware Rec in stderr, got: %q", errOutput) + } +} diff --git a/cmd/myf5/wizard.go b/cmd/myf5/wizard.go index 4a8ce6c..6b6a820 100644 --- a/cmd/myf5/wizard.go +++ b/cmd/myf5/wizard.go @@ -367,6 +367,19 @@ func editWizardField(reader *bufio.Reader, out io.Writer, fieldNum int, opts *Ca opts.Product, err = promptRequiredNavField(reader, out, "Product Name (e.g. BIG-IP LTM, BIG-IP DNS)", opts.Product) case 3: opts.Version, err = promptRequiredNavField(reader, out, "Product Version (e.g. 17.5.1)", opts.Version) + if err == nil && (strings.EqualFold(opts.ProductFamily, "BIG-IP") || strings.Contains(strings.ToUpper(opts.Product), "BIG-IP")) { + lc := bigip.EvaluateLifecycle(opts.Version, opts.Product, time.Now()) + if len(lc.Warnings) > 0 { + fmt.Fprintln(out) + for _, w := range lc.Warnings { + fmt.Fprintf(out, " ⚠️ [WARNING] %s\n", w) + } + if lc.Software.IsEOL && lc.Software.Recommendation != "" { + fmt.Fprintf(out, " Recommendation: %s\n", lc.Software.Recommendation) + } + fmt.Fprintln(out) + } + } case 4: isSub := strings.EqualFold(opts.ProductFamily, "NGINX") || strings.EqualFold(opts.ProductFamily, "Aspen Mesh") if isSub { @@ -580,6 +593,18 @@ func runStep1AutoDetection(ctx context.Context, reader *bufio.Reader, out io.Wri if opts.Hostname != "" { fmt.Fprintf(out, " Hostname : %s\n", opts.Hostname) } + if info.Lifecycle == nil { + info.EvaluateLifecycle(time.Now()) + } + if info.Lifecycle != nil && len(info.Lifecycle.Warnings) > 0 { + fmt.Fprintln(out, "\n ⚠️ Lifecycle & Support Warnings:") + for _, w := range info.Lifecycle.Warnings { + fmt.Fprintf(out, " - %s\n", w) + } + if info.Lifecycle.Software.Recommendation != "" && info.Lifecycle.Software.Warning != "" { + fmt.Fprintf(out, " - Software Rec: %s\n", info.Lifecycle.Software.Recommendation) + } + } fmt.Fprintln(out) } } @@ -611,6 +636,18 @@ func runStep1AutoDetection(ctx context.Context, reader *bufio.Reader, out io.Wri if opts.Hostname != "" { fmt.Fprintf(out, " Hostname : %s\n", opts.Hostname) } + if info.Lifecycle == nil { + info.EvaluateLifecycle(time.Now()) + } + if info.Lifecycle != nil && len(info.Lifecycle.Warnings) > 0 { + fmt.Fprintln(out, "\n ⚠️ Lifecycle & Support Warnings:") + for _, w := range info.Lifecycle.Warnings { + fmt.Fprintf(out, " - %s\n", w) + } + if info.Lifecycle.Software.Recommendation != "" && info.Lifecycle.Software.Warning != "" { + fmt.Fprintf(out, " - Software Rec: %s\n", info.Lifecycle.Software.Recommendation) + } + } fmt.Fprintln(out) } } @@ -658,6 +695,19 @@ func runStep2Entitlement(reader *bufio.Reader, out io.Writer, opts *CaseWizardOp if err != nil { return 0, 0, err } + if strings.EqualFold(opts.ProductFamily, "BIG-IP") || strings.Contains(strings.ToUpper(opts.Product), "BIG-IP") { + lc := bigip.EvaluateLifecycle(opts.Version, opts.Product, time.Now()) + if len(lc.Warnings) > 0 { + fmt.Fprintln(out) + for _, w := range lc.Warnings { + fmt.Fprintf(out, " ⚠️ [WARNING] %s\n", w) + } + if lc.Software.IsEOL && lc.Software.Recommendation != "" { + fmt.Fprintf(out, " Recommendation: %s\n", lc.Software.Recommendation) + } + fmt.Fprintln(out) + } + } field = 4 case 4: isSubscription := strings.EqualFold(opts.ProductFamily, "NGINX") || strings.EqualFold(opts.ProductFamily, "Aspen Mesh") @@ -952,6 +1002,20 @@ func runStep5Review(ctx context.Context, reader *bufio.Reader, out io.Writer, cl fmt.Fprintf(out, " [13] Ticket Ref : (none)\n") } fmt.Fprintln(out, "========================================================================") + if strings.EqualFold(opts.ProductFamily, "BIG-IP") || strings.Contains(strings.ToUpper(opts.Product), "BIG-IP") { + lc := bigip.EvaluateLifecycle(opts.Version, opts.Product, time.Now()) + if len(lc.Warnings) > 0 { + fmt.Fprintln(out, "⚠️ LIFECYCLE WARNING:") + for _, w := range lc.Warnings { + fmt.Fprintf(out, " - %s\n", w) + } + if lc.Software.IsEOL && lc.Software.Recommendation != "" { + fmt.Fprintf(out, " - Recommendation: %s\n", lc.Software.Recommendation) + } + fmt.Fprintln(out, " Note: F5 Support may provide limited assistance for EOL releases.") + fmt.Fprintln(out, "========================================================================") + } + } fmt.Fprintln(out) fmt.Fprintln(out, "SAFETY NOTICE:") fmt.Fprintln(out, " Submitting this case will immediately create a real, visible support") diff --git a/cmd/myf5/wizard_test.go b/cmd/myf5/wizard_test.go index 73adde7..bb85c1a 100644 --- a/cmd/myf5/wizard_test.go +++ b/cmd/myf5/wizard_test.go @@ -355,3 +355,47 @@ func TestRunCaseWizard_StepBackNavigation(t *testing.T) { t.Errorf("found discarded initial subject in dry run payload") } } + +func TestRunCaseWizard_LifecycleWarnings(t *testing.T) { + inputLines := []string{ + "3", // Step 1: skip auto-detect + "BIG-IP", // Step 2: Product family + "BIG-IP 2000s", // Product (legacy hardware EOL) + "15.1.8", // Version (TMOS 15.1.x EOL) + "test-serial-eol", // Serial number + "4", // Step 3: Priority 4 + "1", // Reason 1 + "Testing EOL warnings", // Subject + "Verification of EOL banner", // Description + "testuser@example.com", // Step 4: Email + "", // Contact Method (default) + "", // Timezone (default) + "", // Hostname + "", // Ticket Ref + "d", // Step 5: Dry Run + "n", // Submit now? No + } + + input := strings.Join(inputLines, "\n") + "\n" + in := strings.NewReader(input) + var out bytes.Buffer + + err := RunCaseWizard(context.Background(), in, &out, nil) + if err != nil { + t.Fatalf("wizard returned unexpected error: %v", err) + } + + outStr := out.String() + if !strings.Contains(outStr, "TMOS version 15.1.8 reached End of Life") { + t.Errorf("expected TMOS EOL warning in wizard output, got:\n%s", outStr) + } + if !strings.Contains(outStr, "Hardware platform BIG-IP 2000s reached End of Life") { + t.Errorf("expected hardware EOL warning in wizard output, got:\n%s", outStr) + } + if !strings.Contains(outStr, "⚠️ LIFECYCLE WARNING:") { + t.Errorf("expected LIFECYCLE WARNING header in review confirmation, got:\n%s", outStr) + } + if !strings.Contains(outStr, "F5 Support may provide limited assistance for EOL releases") { + t.Errorf("expected EOL limited assistance notice in review confirmation, got:\n%s", outStr) + } +} diff --git a/pkg/bigip/info.go b/pkg/bigip/info.go index e7a0193..f654168 100644 --- a/pkg/bigip/info.go +++ b/pkg/bigip/info.go @@ -16,6 +16,7 @@ import ( "sort" "strconv" "strings" + "time" "golang.org/x/crypto/ssh" @@ -36,6 +37,7 @@ type SystemInfo struct { ProvisionedModules map[string]string `json:"provisionedModules,omitempty"` CaseAttributes map[string]string `json:"caseAttributes,omitempty"` Source string `json:"source,omitempty"` + Lifecycle *LifecycleInfo `json:"lifecycle,omitempty"` } func isInternalModule(mod string) bool { @@ -85,12 +87,24 @@ func (s *SystemInfo) PopulateCaseAttributes() { } attrs["product"] = product - if strings.Contains(s.Hostname, ".ec2.internal") || strings.Contains(s.Hostname, ".compute.internal") || s.Platform == "Z100" { - attrs["cloudProvider"] = "AWS" - } else if strings.Contains(s.Hostname, "azure") || strings.Contains(s.Hostname, "trafficmanager.net") { - attrs["cloudProvider"] = "Azure" - } else if strings.Contains(s.Hostname, "google") || strings.Contains(s.Hostname, "gce") { - attrs["cloudProvider"] = "Google" + cloud := "" + if rec, ok := LookupPlatform(s.Platform); ok { + switch rec.Code { + case "Z100", "Z100A": + cloud = "AWS" + } + } + if cloud == "" { + if strings.Contains(s.Hostname, ".ec2.internal") || strings.Contains(s.Hostname, ".compute.internal") || s.Platform == "Z100" { + cloud = "AWS" + } else if strings.Contains(s.Hostname, "azure") || strings.Contains(s.Hostname, "trafficmanager.net") { + cloud = "Azure" + } else if strings.Contains(s.Hostname, "google") || strings.Contains(s.Hostname, "gce") { + cloud = "Google" + } + } + if cloud != "" { + attrs["cloudProvider"] = cloud } s.CaseAttributes = attrs @@ -117,7 +131,13 @@ func (s *SystemInfo) Summary() string { b.WriteString(fmt.Sprintf(" Version: %s\n", ver)) } if s.Platform != "" { - b.WriteString(fmt.Sprintf(" Platform: %s\n", s.Platform)) + platDisplay := s.Platform + if rec, ok := LookupPlatform(s.Platform); ok && rec.Model != "" { + if !strings.Contains(s.Platform, rec.Model) && s.Platform != rec.Model { + platDisplay = fmt.Sprintf("%s (%s)", s.Platform, rec.Model) + } + } + b.WriteString(fmt.Sprintf(" Platform: %s\n", platDisplay)) } if s.ChassisSerialNumber != "" { b.WriteString(fmt.Sprintf(" Chassis Serial: %s\n", s.ChassisSerialNumber)) @@ -157,6 +177,32 @@ func (s *SystemInfo) Summary() string { b.WriteString(fmt.Sprintf(" %-18s: %s\n", k, s.CaseAttributes[k])) } } + if s.Lifecycle == nil && (s.Version != "" || s.Platform != "") { + s.EvaluateLifecycle(time.Now()) + } + if s.Lifecycle != nil { + if len(s.Lifecycle.Warnings) > 0 { + b.WriteString("\nLifecycle & Support Warnings:\n") + for _, w := range s.Lifecycle.Warnings { + b.WriteString(fmt.Sprintf(" ⚠️ [WARNING] %s\n", w)) + } + if s.Lifecycle.Software.Recommendation != "" && s.Lifecycle.Software.Warning != "" { + b.WriteString(fmt.Sprintf(" Software Rec: %s\n", s.Lifecycle.Software.Recommendation)) + } + } else { + b.WriteString("\nLifecycle & Support Status:\n") + if s.Lifecycle.Software.Status != StatusUnknown && s.Lifecycle.Software.Status != "" { + b.WriteString(fmt.Sprintf(" Software (%-9s): %s (%s)\n", s.Lifecycle.Software.Version, s.Lifecycle.Software.Status, s.Lifecycle.Software.Recommendation)) + } + if s.Lifecycle.Hardware.Status != StatusUnknown && s.Lifecycle.Hardware.Status != "" { + fam := s.Lifecycle.Hardware.Family + if fam == "" { + fam = s.Lifecycle.Hardware.Platform + } + b.WriteString(fmt.Sprintf(" Hardware (%-9s): %s\n", fam, s.Lifecycle.Hardware.Status)) + } + } + } return b.String() } @@ -388,6 +434,7 @@ func InspectQKViewReader(r io.Reader) (*SystemInfo, error) { } info.PopulateCaseAttributes() + info.EvaluateLifecycle(time.Now()) return info, nil } @@ -464,6 +511,7 @@ func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) { } info.PopulateCaseAttributes() + info.EvaluateLifecycle(time.Now()) return info, nil } @@ -549,6 +597,7 @@ func GetSystemInfoSSH(ctx context.Context, opts SCPOptions) (*SystemInfo, error) info := parseTmshOutput(outStr) info.Source = fmt.Sprintf("appliance: %s (SSH)", host) info.PopulateCaseAttributes() + info.EvaluateLifecycle(time.Now()) return info, nil } @@ -669,7 +718,8 @@ func parseTmshOutput(output string) *SystemInfo { chassisRe := regexp.MustCompile(`(?i)(?:Chassis Serial|Serial Number)\s+([^\s]+)`) regKeyRe := regexp.MustCompile(`(?i)Registration Key\s+([A-Z0-9-]+)`) - platformRe := regexp.MustCompile(`(?i)(?:Platform\s+Name|Platform)\s+([^\s]+)`) + platformRe := regexp.MustCompile(`(?i)(?:Platform\s+Name|Platform)\s+([^\r\n]+)`) + hardwareTypeRe := regexp.MustCompile(`(?i)^\s*Type\s+([A-Z0-9]+)`) macRe := regexp.MustCompile(`(?i)Base MAC\s+([0-9a-fA-F:]+)`) versionRe := regexp.MustCompile(`(?i)Version\s+([0-9.]+)`) buildRe := regexp.MustCompile(`(?i)Build\s+([0-9.]+)`) @@ -716,6 +766,12 @@ func parseTmshOutput(output string) *SystemInfo { info.Platform = val } } + if m := hardwareTypeRe.FindStringSubmatch(line); len(m) > 1 { + typeCode := strings.ToUpper(strings.TrimSpace(m[1])) + if _, ok := LookupPlatformByID(typeCode); ok { + info.Platform = typeCode + } + } if m := versionRe.FindStringSubmatch(line); len(m) > 1 && info.Version == "" { info.Version = m[1] } diff --git a/pkg/bigip/info_test.go b/pkg/bigip/info_test.go index 045e7f3..653ef07 100644 --- a/pkg/bigip/info_test.go +++ b/pkg/bigip/info_test.go @@ -446,3 +446,109 @@ func TestGetSystemInfo_RestMock(t *testing.T) { t.Errorf("CaseAttributes[serialNumber] = %q; want chs-rest-test-1", info.CaseAttributes["serialNumber"]) } } + +func TestSystemInfo_SummaryLifecycle(t *testing.T) { + // Case 1: EOL Software and EOL Hardware + eolInfo := &SystemInfo{ + Product: "BIG-IP", + Version: "15.1.8", + Platform: "2000s", + } + summary := eolInfo.Summary() + if !strings.Contains(summary, "Lifecycle & Support Warnings:") { + t.Errorf("expected Lifecycle & Support Warnings in summary, got:\n%s", summary) + } + if !strings.Contains(summary, "TMOS version 15.1.8 reached End of Life") { + t.Errorf("expected TMOS EOL warning in summary, got:\n%s", summary) + } + if !strings.Contains(summary, "Hardware platform 2000s reached End of Life") { + t.Errorf("expected hardware EOL warning in summary, got:\n%s", summary) + } + if !strings.Contains(summary, "Software Rec:") { + t.Errorf("expected Software Rec in summary, got:\n%s", summary) + } + if strings.Contains(summary, "Hardware Rec:") { + t.Errorf("expected no Hardware Rec in summary, got:\n%s", summary) + } + + // Case 2: Supported Software and Virtual Edition Hardware + suppInfo := &SystemInfo{ + Product: "BIG-IP", + Version: "17.5.1", + Platform: "Z100", + } + suppSummary := suppInfo.Summary() + if !strings.Contains(suppSummary, "Lifecycle & Support Status:") { + t.Errorf("expected Lifecycle & Support Status in summary, got:\n%s", suppSummary) + } + if !strings.Contains(suppSummary, "Software (17.5.1") || !strings.Contains(suppSummary, "Supported") { + t.Errorf("expected Software Supported in summary, got:\n%s", suppSummary) + } + if !strings.Contains(suppSummary, "Hardware (Virtual Edition): Supported") { + t.Errorf("expected Hardware Supported in summary, got:\n%s", suppSummary) + } + if strings.Contains(suppSummary, "[WARNING]") { + t.Errorf("did not expect warnings in supported summary, got:\n%s", suppSummary) + } +} + +func TestParseTmshOutput_WithHardwareType(t *testing.T) { + output := ` +Sys::Hardware +Chassis Information + Chassis Serial chs-i5800-999 +Platform Information + Platform Name BIG-IP i5800 + Base MAC 00:01:d7:aa:bb:cc +System Information + Type C119 + Appliance Series i5000 + +Sys::Version +Main Package + Product BIG-IP + Version 17.1.1 + Build 0.0.4 + Edition Final + +Sys::License +Active License + Registration Key REG-TMSH-KEY-999 +` + info := parseTmshOutput(output) + info.PopulateCaseAttributes() + + if info.Platform != "C119" { + t.Errorf("Platform = %q; want C119", info.Platform) + } + if info.ChassisSerialNumber != "chs-i5800-999" { + t.Errorf("ChassisSerialNumber = %q; want chs-i5800-999", info.ChassisSerialNumber) + } + summary := info.Summary() + if !strings.Contains(summary, "Platform: C119 (BIG-IP i5600 / i5800)") { + t.Errorf("Summary() did not contain enriched platform:\n%s", summary) + } +} + +func TestSystemInfo_SummaryPlatformEnrichment(t *testing.T) { + // Case 1: Raw platform code C119 + info1 := &SystemInfo{Platform: "C119"} + sum1 := info1.Summary() + if !strings.Contains(sum1, "Platform: C119 (BIG-IP i5600 / i5800)") { + t.Errorf("Summary() did not enrich C119:\n%s", sum1) + } + + // Case 2: Raw platform code Z100 + info2 := &SystemInfo{Platform: "Z100"} + sum2 := info2.Summary() + if !strings.Contains(sum2, "Platform: Z100 (BIG-IP Virtual Edition)") { + t.Errorf("Summary() did not enrich Z100:\n%s", sum2) + } + + // Case 3: Already fully named platform + info3 := &SystemInfo{Platform: "BIG-IP Virtual Edition"} + sum3 := info3.Summary() + if !strings.Contains(sum3, "Platform: BIG-IP Virtual Edition") || strings.Contains(sum3, "BIG-IP Virtual Edition (BIG-IP Virtual Edition)") { + t.Errorf("Summary() duplicate enrichment for BIG-IP Virtual Edition:\n%s", sum3) + } +} diff --git a/pkg/bigip/lifecycle.go b/pkg/bigip/lifecycle.go new file mode 100644 index 0000000..ae235b9 --- /dev/null +++ b/pkg/bigip/lifecycle.go @@ -0,0 +1,675 @@ +package bigip + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +// LifecycleStatus defines the support and lifecycle state of a TMOS release or hardware platform. +type LifecycleStatus string + +const ( + StatusSupported LifecycleStatus = "Supported" + StatusApproachingEOL LifecycleStatus = "Approaching EOL" + StatusExtendedSupport LifecycleStatus = "Extended Support" + StatusEOL LifecycleStatus = "End of Life" + StatusUnknown LifecycleStatus = "Unknown" +) + +// Policy and article reference URLs +const ( + URLPolicyTMOS = "https://my.f5.com/manage/s/article/K5903" + URLPolicyHardware = "https://my.f5.com/manage/s/article/K4309" + URLArticleK13144 = "https://my.f5.com/manage/s/article/K13144" +) + +// PlatformCategory groups F5 platforms into distinct lifecycle tracks under K4309. +type PlatformCategory string + +const ( + CategoryVirtualEdition PlatformCategory = "Virtual Edition" + CategoryISeriesMidrange PlatformCategory = "iSeries Midrange" + CategoryISeriesHighEnd PlatformCategory = "iSeries High-End" + CategoryModern PlatformCategory = "Modern Architecture" // rSeries / VELOS + CategoryVIPRIONMidrange PlatformCategory = "VIPRION Midrange" + CategoryVIPRIONHighEnd PlatformCategory = "VIPRION High-End" + CategoryVIPRIONLegacy PlatformCategory = "VIPRION Legacy" + CategoryLegacyAppliance PlatformCategory = "Legacy Hardware" +) + +// PlatformIDRecord links an internal F5 platform ID code (e.g. C119) from article K13144 +// to its commercial marketing model name, product family, and hardware category. +type PlatformIDRecord struct { + Code string `json:"code"` + Model string `json:"model"` + Family string `json:"family"` + IsVirtual bool `json:"isVirtual"` + Category PlatformCategory `json:"category"` +} + +var k13144PlatformMap = map[string]PlatformIDRecord{ + // Virtual Edition (K13144 / Cloud / Hypervisors) + "Z100": {Code: "Z100", Model: "BIG-IP Virtual Edition", Family: "Virtual Edition", IsVirtual: true, Category: CategoryVirtualEdition}, + "Z100A": {Code: "Z100A", Model: "BIG-IP Virtual Edition (AWS)", Family: "Virtual Edition", IsVirtual: true, Category: CategoryVirtualEdition}, + "Z100H": {Code: "Z100H", Model: "BIG-IP Virtual Edition (Hyper-V)", Family: "Virtual Edition", IsVirtual: true, Category: CategoryVirtualEdition}, + "Z100K": {Code: "Z100K", Model: "BIG-IP Virtual Edition (KVM)", Family: "Virtual Edition", IsVirtual: true, Category: CategoryVirtualEdition}, + "V100": {Code: "V100", Model: "BIG-IP Virtual Edition (ESXi)", Family: "Virtual Edition", IsVirtual: true, Category: CategoryVirtualEdition}, + + // iSeries Midrange (K13144 / K4309) + "C115": {Code: "C115", Model: "BIG-IP i4600 / i4800", Family: "BIG-IP iSeries", Category: CategoryISeriesMidrange}, + "C116": {Code: "C116", Model: "BIG-IP i10600 / i10800", Family: "BIG-IP iSeries", Category: CategoryISeriesMidrange}, + "C117": {Code: "C117", Model: "BIG-IP i850 / i2600 / i2800", Family: "BIG-IP iSeries", Category: CategoryISeriesMidrange}, + "C118": {Code: "C118", Model: "BIG-IP i7600 / i7800", Family: "BIG-IP iSeries", Category: CategoryISeriesMidrange}, + "C119": {Code: "C119", Model: "BIG-IP i5600 / i5800", Family: "BIG-IP iSeries", Category: CategoryISeriesMidrange}, + "C123": {Code: "C123", Model: "BIG-IP i11400-T / i11600-DS / i11800-DS", Family: "BIG-IP iSeries", Category: CategoryISeriesMidrange}, + + // iSeries High-End (K13144 / K4309) + "D116": {Code: "D116", Model: "BIG-IP i15600 / i15800", Family: "BIG-IP iSeries (High-End)", Category: CategoryISeriesHighEnd}, + + // rSeries (F5OS) + "C128": {Code: "C128", Model: "F5 r10600 / r10800", Family: "F5 rSeries", Category: CategoryModern}, + "C129": {Code: "C129", Model: "F5 r5600 / r5800", Family: "F5 rSeries", Category: CategoryModern}, + "C130": {Code: "C130", Model: "F5 r2600 / r2800", Family: "F5 rSeries", Category: CategoryModern}, + "C131": {Code: "C131", Model: "F5 r4600 / r4800", Family: "F5 rSeries", Category: CategoryModern}, + "C138": {Code: "C138", Model: "F5 r12600-DS / r12800-DS / r12900-DS", Family: "F5 rSeries", Category: CategoryModern}, + + // VELOS (F5OS) + "CX410": {Code: "CX410", Model: "VELOS CX410 Chassis", Family: "F5 VELOS", Category: CategoryModern}, + "BX110": {Code: "BX110", Model: "VELOS BX110 Blade", Family: "F5 VELOS", Category: CategoryModern}, + + // VIPRION Chassis + "F100": {Code: "F100", Model: "VIPRION C2400 Chassis", Family: "VIPRION", Category: CategoryVIPRIONMidrange}, + "D114": {Code: "D114", Model: "VIPRION C2200 Chassis", Family: "VIPRION", Category: CategoryVIPRIONMidrange}, + "J100": {Code: "J100", Model: "VIPRION C4400 Chassis", Family: "VIPRION (Legacy)", Category: CategoryVIPRIONLegacy}, + "J102": {Code: "J102", Model: "VIPRION C4480 Chassis", Family: "VIPRION (High-End)", Category: CategoryVIPRIONHighEnd}, + "S100": {Code: "S100", Model: "VIPRION C4800 Chassis", Family: "VIPRION (High-End)", Category: CategoryVIPRIONHighEnd}, + + // VIPRION Blades + "A100": {Code: "A100", Model: "VIPRION B4100 Blade", Family: "VIPRION (Legacy)", Category: CategoryVIPRIONLegacy}, + "A107": {Code: "A107", Model: "VIPRION B4200 Blade", Family: "VIPRION (Legacy)", Category: CategoryVIPRIONLegacy}, + "A108": {Code: "A108", Model: "VIPRION B4300 Blade", Family: "VIPRION (Legacy)", Category: CategoryVIPRIONLegacy}, + "A109": {Code: "A109", Model: "VIPRION B2100 Blade", Family: "VIPRION (Legacy)", Category: CategoryVIPRIONLegacy}, + "A110": {Code: "A110", Model: "VIPRION B4340N Blade", Family: "VIPRION (Legacy)", Category: CategoryVIPRIONLegacy}, + "A112": {Code: "A112", Model: "VIPRION B2250 Blade", Family: "VIPRION", Category: CategoryVIPRIONMidrange}, + "A113": {Code: "A113", Model: "VIPRION B2150 Blade", Family: "VIPRION", Category: CategoryVIPRIONMidrange}, + "A114": {Code: "A114", Model: "VIPRION B4450 Blade", Family: "VIPRION (High-End)", Category: CategoryVIPRIONHighEnd}, + + // Legacy Hardware Appliances (K4309: EOL past End of Technical Support) + "C102": {Code: "C102", Model: "BIG-IP 1600", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "C103": {Code: "C103", Model: "BIG-IP 3600", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "C106": {Code: "C106", Model: "BIG-IP 3900 / Enterprise Manager 4000", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "C109": {Code: "C109", Model: "BIG-IP 5000s / 5200v / 5250v", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "C112": {Code: "C112", Model: "BIG-IP 2000s / 2200s", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "C113": {Code: "C113", Model: "BIG-IP 4000s / 4200v", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "C114": {Code: "C114", Model: "BIG-IP 800", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "D104": {Code: "D104", Model: "BIG-IP 6900", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "D106": {Code: "D106", Model: "BIG-IP 8900", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "D110": {Code: "D110", Model: "BIG-IP 7000s / 7200v / BIG-IQ 7000", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "D111": {Code: "D111", Model: "BIG-IP 12250v", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "D112": {Code: "D112", Model: "BIG-IP 10350v", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "D113": {Code: "D113", Model: "BIG-IP 10000s / 10200v", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "E101": {Code: "E101", Model: "BIG-IP 11000", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, + "E102": {Code: "E102", Model: "BIG-IP 11050", Family: "BIG-IP Legacy Hardware", Category: CategoryLegacyAppliance}, +} + +// LookupPlatformByID returns the K13144 platform metadata for an exact F5 platform ID code +// (e.g. "C119", "C113", "Z100", "D110"). +func LookupPlatformByID(code string) (PlatformIDRecord, bool) { + clean := strings.ToUpper(strings.TrimSpace(code)) + clean = strings.TrimPrefix(clean, "PLATFORM=") + rec, ok := k13144PlatformMap[clean] + return rec, ok +} + +var platformTokenRe = regexp.MustCompile(`\b([ACDEFSJVZ]\d{3}[A-Z]?|BX110|CX410)\b`) + +// LookupPlatform searches for a known K13144 platform code in the given string, +// handling raw codes ("C119"), QKView key-values ("platform=C119"), or formatted +// strings ("BIG-IP i5800 (C119)"). +func LookupPlatform(platform string) (PlatformIDRecord, bool) { + upper := strings.ToUpper(strings.TrimSpace(platform)) + if upper == "" { + return PlatformIDRecord{}, false + } + // Direct code match (e.g. "C119" or "platform=C119") + if rec, ok := LookupPlatformByID(upper); ok { + return rec, true + } + // Search for platform code token within the string + matches := platformTokenRe.FindAllString(upper, -1) + for _, m := range matches { + if rec, ok := k13144PlatformMap[m]; ok { + return rec, true + } + } + return PlatformIDRecord{}, false +} + +// SoftwareLifecycle captures the lifecycle milestones for a TMOS version release. +type SoftwareLifecycle struct { + Version string `json:"version"` + Branch string `json:"branch,omitempty"` + Status LifecycleStatus `json:"status"` + IsEOL bool `json:"isEol"` + EoSD string `json:"eosd,omitempty"` // End of Software Development (YYYY-MM-DD) + EoTS string `json:"eots,omitempty"` // End of Technical Support (YYYY-MM-DD) + NoticeURL string `json:"noticeUrl,omitempty"` + Recommendation string `json:"recommendation,omitempty"` + Warning string `json:"warning,omitempty"` +} + +// HardwareLifecycle captures the lifecycle milestones for a BIG-IP physical or virtual platform. +type HardwareLifecycle struct { + Platform string `json:"platform"` + Family string `json:"family,omitempty"` + IsVirtual bool `json:"isVirtual"` + Status LifecycleStatus `json:"status"` + IsEOL bool `json:"isEol"` + EoS string `json:"eos,omitempty"` // End of Sale (YYYY-MM-DD) + EoNSS string `json:"eonss,omitempty"` // End of New Software Support (YYYY-MM-DD) + EoSS string `json:"eoss,omitempty"` // End of Software Support (YYYY-MM-DD) + EoTS string `json:"eots,omitempty"` // End of Technical Support (YYYY-MM-DD) + NoticeURL string `json:"noticeUrl,omitempty"` + Recommendation string `json:"recommendation,omitempty"` + Warning string `json:"warning,omitempty"` +} + +// LifecycleInfo aggregates both software and hardware lifecycle assessments. +type LifecycleInfo struct { + Software SoftwareLifecycle `json:"software"` + Hardware HardwareLifecycle `json:"hardware"` + Warnings []string `json:"warnings,omitempty"` +} + +var versionExtractorRe = regexp.MustCompile(`(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?`) + +// ParseTMOSVersion extracts major, minor, and patch numbers from a version string. +func ParseTMOSVersion(v string) (major, minor, patch int, ok bool) { + m := versionExtractorRe.FindStringSubmatch(v) + if len(m) < 3 { + return 0, 0, 0, false + } + var err error + major, err = strconv.Atoi(m[1]) + if err != nil { + return 0, 0, 0, false + } + minor, err = strconv.Atoi(m[2]) + if err != nil { + return 0, 0, 0, false + } + if len(m) > 3 && m[3] != "" { + patch, _ = strconv.Atoi(m[3]) + } + return major, minor, patch, true +} + +// EvaluateTMOSLifecycle checks a TMOS version against F5 Software Lifecycle Policy K5903. +func EvaluateTMOSLifecycle(version string, asOf time.Time) SoftwareLifecycle { + cleanVer := strings.TrimSpace(version) + res := SoftwareLifecycle{ + Version: cleanVer, + NoticeURL: URLPolicyTMOS, + Status: StatusUnknown, + } + + if cleanVer == "" { + res.Status = StatusUnknown + return res + } + + major, minor, _, ok := ParseTMOSVersion(cleanVer) + if !ok { + res.Status = StatusUnknown + return res + } + + res.Branch = fmt.Sprintf("%d.%d", major, minor) + + // Define milestone dates based on K5903 / K8986 + var ( + eosdDate time.Time + eotsDate time.Time + isLTS bool + hasRecord bool + ) + + switch { + case major < 13: + // TMOS 11.x and 12.x reached End of Life in 2020-2022 + eosdDate = time.Date(2022, time.December, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = true + hasRecord = true + + case major == 13: + // TMOS 13.1.x: EoSD/EoTS Dec 31, 2023 + eosdDate = time.Date(2023, time.December, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = (minor == 1) + hasRecord = true + + case major == 14: + // TMOS 14.1.x: EoSD/EoTS Dec 31, 2023 + eosdDate = time.Date(2023, time.December, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = (minor == 1) + hasRecord = true + + case major == 15: + if minor == 0 { + eosdDate = time.Date(2021, time.February, 28, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = false + } else { + // TMOS 15.1.x: EoSD/EoTS Dec 31, 2024 + eosdDate = time.Date(2024, time.December, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = true + } + hasRecord = true + + case major == 16: + if minor == 0 { + eosdDate = time.Date(2021, time.December, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = false + } else { + // TMOS 16.1.x: Standard EoSD/EoTS July 31, 2025 (FIPS extensions up to July 31, 2026) + eosdDate = time.Date(2025, time.July, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = true + } + hasRecord = true + + case major == 17: + if minor == 0 { + eosdDate = time.Date(2023, time.July, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = false + } else { + // TMOS 17.1.x / 17.5.x: LTS EoSD/EoTS March 31, 2027 + eosdDate = time.Date(2027, time.March, 31, 23, 59, 59, 0, time.UTC) + eotsDate = eosdDate + isLTS = true + } + hasRecord = true + + default: + // Future releases (e.g., BIG-IP 20.x, 21.x): actively supported + res.Status = StatusSupported + res.Recommendation = "Current release stream." + return res + } + + if hasRecord { + res.EoSD = eosdDate.Format("2006-01-02") + res.EoTS = eotsDate.Format("2006-01-02") + + if asOf.After(eotsDate) { + res.Status = StatusEOL + res.IsEOL = true + res.Warning = fmt.Sprintf("TMOS version %s reached End of Life (EoSD/EoTS) on %s (K5903).", cleanVer, res.EoSD) + res.Recommendation = "Upgrade to an actively supported LTS release (TMOS 17.1.x+) to receive software fixes and support." + } else if asOf.AddDate(0, 3, 0).After(eosdDate) { + res.Status = StatusApproachingEOL + res.IsEOL = false + res.Warning = fmt.Sprintf("TMOS version %s is approaching End of Life on %s (K5903).", cleanVer, res.EoSD) + res.Recommendation = "Plan upgrade to an actively supported release (TMOS 17.1.x+)." + } else { + res.Status = StatusSupported + res.IsEOL = false + if isLTS { + res.Recommendation = fmt.Sprintf("LTS release with standard support active until %s.", res.EoSD) + } else { + res.Recommendation = fmt.Sprintf("Standard support active until %s.", res.EoSD) + } + } + } + + return res +} + +// EvaluatePlatformLifecycle checks a hardware platform name or platform ID (K13144) +// against F5 Hardware Product Lifecycle Support Policy K4309. +func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycle { + cleanPlat := strings.TrimSpace(platform) + if cleanPlat == "" { + return HardwareLifecycle{ + NoticeURL: URLPolicyHardware, + Status: StatusUnknown, + } + } + + upper := strings.ToUpper(cleanPlat) + + res := HardwareLifecycle{ + Platform: cleanPlat, + NoticeURL: URLPolicyHardware, + Status: StatusUnknown, + } + + // 1. Check if platform matches a known F5 Platform ID code from K13144 + if rec, ok := LookupPlatform(cleanPlat); ok { + res.Family = rec.Family + res.IsVirtual = rec.IsVirtual + + // If the platform input does not already mention the model, enrich display with model + displayPlat := cleanPlat + if !strings.Contains(upper, strings.ToUpper(rec.Model)) && !rec.IsVirtual { + displayPlat = fmt.Sprintf("%s (%s)", cleanPlat, rec.Model) + } + + switch rec.Category { + case CategoryVirtualEdition: + res.Status = StatusSupported + return res + + case CategoryModern: + res.Status = StatusSupported + return res + + case CategoryISeriesMidrange: + res.EoS = "2024-01-01" + res.EoNSS = "2026-01-01" + res.EoSS = "2027-01-01" + res.EoTS = "2031-01-01" + + eossDate := time.Date(2027, time.January, 1, 23, 59, 59, 0, time.UTC) + eonssDate := time.Date(2026, time.January, 1, 23, 59, 59, 0, time.UTC) + + if asOf.After(eossDate) { + res.Status = StatusExtendedSupport + res.Warning = fmt.Sprintf("Platform %s is in Extended Support phase (EoSS reached 2027-01-01; EoTS: 2031-01-01). No software maintenance or bug fixes are developed for this platform (K4309).", displayPlat) + } else if asOf.After(eonssDate) { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("Platform %s reached End of New Software Support (EoNSS: 2026-01-01) and reaches End of Software Support on 2027-01-01 (K4309).", displayPlat) + } else { + res.Status = StatusSupported + } + return res + + case CategoryISeriesHighEnd: + res.EoS = "2027-01-01" + res.EoNSS = "2028-01-01" + res.EoSS = "2029-01-01" + res.EoTS = "2030-01-01" + + eosDate := time.Date(2027, time.January, 1, 23, 59, 59, 0, time.UTC) + if asOf.After(eosDate) { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("Platform %s reached End of Sale on 2027-01-01 (K4309).", displayPlat) + } else { + res.Status = StatusSupported + } + return res + + case CategoryVIPRIONMidrange: + res.EoS = "2023-04-01" + res.EoNSS = "2025-04-01" + res.EoSS = "2026-04-01" + res.EoTS = "2030-04-01" + + eossDate := time.Date(2026, time.April, 1, 23, 59, 59, 0, time.UTC) + if asOf.After(eossDate) { + res.Status = StatusExtendedSupport + res.Warning = fmt.Sprintf("VIPRION platform %s is in Extended Support phase (EoSS reached 2026-04-01; EoTS: 2030-04-01). No software maintenance or bug fixes are developed for this platform (K4309).", displayPlat) + } else { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("VIPRION platform %s approaches End of Software Support on 2026-04-01 (K4309).", displayPlat) + } + return res + + case CategoryVIPRIONHighEnd: + res.EoS = "2026-01-01" + res.EoNSS = "2028-01-01" + res.EoSS = "2029-01-01" + res.EoTS = "2033-01-01" + + eosDate := time.Date(2026, time.January, 1, 23, 59, 59, 0, time.UTC) + eossDate := time.Date(2029, time.January, 1, 23, 59, 59, 0, time.UTC) + + if asOf.After(eossDate) { + res.Status = StatusExtendedSupport + res.Warning = fmt.Sprintf("VIPRION platform %s reached EoSS on 2029-01-01 (K4309).", displayPlat) + } else if asOf.After(eosDate) { + res.Status = StatusSupported + res.Warning = fmt.Sprintf("VIPRION platform %s reached End of Sale on 2026-01-01 (K4309). Regular software support active until 2029-01-01.", displayPlat) + } else { + res.Status = StatusSupported + } + return res + + case CategoryVIPRIONLegacy: + res.Status = StatusEOL + res.IsEOL = true + res.Warning = fmt.Sprintf("VIPRION platform %s reached End of Life (past End of Technical Support, K4309).", displayPlat) + return res + + case CategoryLegacyAppliance: + res.Status = StatusEOL + res.IsEOL = true + res.Warning = fmt.Sprintf("Hardware platform %s reached End of Life (past End of Technical Support, K4309). No RMA or TAC support is available.", displayPlat) + return res + } + } + + // 2. Fallback to model name pattern matching when no platform ID was matched + + // Virtual Edition (Cloud, Hypervisors) + if isVirtualPlatform(upper) { + res.Family = "Virtual Edition" + res.IsVirtual = true + res.Status = StatusSupported + return res + } + + // Modern Platforms: rSeries & VELOS + if isModernPlatform(upper) { + if strings.Contains(upper, "CX") || strings.Contains(upper, "BX") || strings.Contains(upper, "VELOS") { + res.Family = "F5 VELOS" + } else { + res.Family = "F5 rSeries" + } + res.Status = StatusSupported + return res + } + + // iSeries Midrange: i2600, i2800, i4600, i4800, i5600, i5800, i7600, i7800, i10600, i10800, i11400, i11600, i11800 + if isISeriesMidrange(upper) { + res.Family = "BIG-IP iSeries" + res.EoS = "2024-01-01" + res.EoNSS = "2026-01-01" + res.EoSS = "2027-01-01" + res.EoTS = "2031-01-01" + + eossDate := time.Date(2027, time.January, 1, 23, 59, 59, 0, time.UTC) + eonssDate := time.Date(2026, time.January, 1, 23, 59, 59, 0, time.UTC) + + if asOf.After(eossDate) { + res.Status = StatusExtendedSupport + res.Warning = fmt.Sprintf("Platform %s is in Extended Support phase (EoSS reached 2027-01-01; EoTS: 2031-01-01). No software maintenance or bug fixes are developed for this platform (K4309).", cleanPlat) + } else if asOf.After(eonssDate) { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("Platform %s reached End of New Software Support (EoNSS: 2026-01-01) and reaches End of Software Support on 2027-01-01 (K4309).", cleanPlat) + } else { + res.Status = StatusSupported + } + return res + } + + // iSeries High-End: i15600, i15800 + if isISeriesHighEnd(upper) { + res.Family = "BIG-IP iSeries (High-End)" + res.EoS = "2027-01-01" + res.EoNSS = "2028-01-01" + res.EoSS = "2029-01-01" + res.EoTS = "2030-01-01" + + eosDate := time.Date(2027, time.January, 1, 23, 59, 59, 0, time.UTC) + if asOf.After(eosDate) { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("Platform %s reached End of Sale on 2027-01-01 (K4309).", cleanPlat) + } else { + res.Status = StatusSupported + } + return res + } + + // VIPRION Midrange: C2400, C2200, B2150, B2250 + if isVIPRIONMidrange(upper) { + res.Family = "VIPRION" + res.EoS = "2023-04-01" + res.EoNSS = "2025-04-01" + res.EoSS = "2026-04-01" + res.EoTS = "2030-04-01" + + eossDate := time.Date(2026, time.April, 1, 23, 59, 59, 0, time.UTC) + if asOf.After(eossDate) { + res.Status = StatusExtendedSupport + res.Warning = fmt.Sprintf("VIPRION platform %s is in Extended Support phase (EoSS reached 2026-04-01; EoTS: 2030-04-01). No software maintenance or bug fixes are developed for this platform (K4309).", cleanPlat) + } else { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("VIPRION platform %s approaches End of Software Support on 2026-04-01 (K4309).", cleanPlat) + } + return res + } + + // VIPRION High-End: C4800, C4480, B4450, B4460 + if isVIPRIONHighEnd(upper) { + res.Family = "VIPRION (High-End)" + res.EoS = "2026-01-01" + res.EoNSS = "2028-01-01" + res.EoSS = "2029-01-01" + res.EoTS = "2033-01-01" + + eosDate := time.Date(2026, time.January, 1, 23, 59, 59, 0, time.UTC) + eossDate := time.Date(2029, time.January, 1, 23, 59, 59, 0, time.UTC) + + if asOf.After(eossDate) { + res.Status = StatusExtendedSupport + res.Warning = fmt.Sprintf("VIPRION platform %s reached EoSS on 2029-01-01 (K4309).", cleanPlat) + } else if asOf.After(eosDate) { + res.Status = StatusSupported + res.Warning = fmt.Sprintf("VIPRION platform %s reached End of Sale on 2026-01-01 (K4309). Regular software support active until 2029-01-01.", cleanPlat) + } else { + res.Status = StatusSupported + } + return res + } + + // VIPRION Legacy Blades: B2100, B4300, B4100, B4200 + if strings.Contains(upper, "B2100") || strings.Contains(upper, "B4300") || strings.Contains(upper, "B4100") || strings.Contains(upper, "B4200") { + res.Family = "VIPRION (Legacy)" + res.Status = StatusEOL + res.IsEOL = true + res.Warning = fmt.Sprintf("VIPRION blade %s reached End of Life (past End of Technical Support, K4309).", cleanPlat) + return res + } + + // Legacy Hardware (2000s, 4000s, 5000s, 7000s, 10000s, 12000s, etc.) + if isLegacyPlatform(upper) { + res.Family = "BIG-IP Legacy Hardware" + res.Status = StatusEOL + res.IsEOL = true + res.Warning = fmt.Sprintf("Hardware platform %s reached End of Life (past End of Technical Support, K4309). No RMA or TAC support is available.", cleanPlat) + return res + } + + res.Status = StatusUnknown + return res +} + +// EvaluateLifecycle evaluates both software and hardware lifecycle statuses. +func EvaluateLifecycle(version, platform string, asOf time.Time) LifecycleInfo { + sw := EvaluateTMOSLifecycle(version, asOf) + hw := EvaluatePlatformLifecycle(platform, asOf) + + var warnings []string + if sw.Warning != "" { + warnings = append(warnings, sw.Warning) + } + if hw.Warning != "" { + warnings = append(warnings, hw.Warning) + } + + return LifecycleInfo{ + Software: sw, + Hardware: hw, + Warnings: warnings, + } +} + +// EvaluateLifecycle updates SystemInfo with lifecycle metadata. +func (s *SystemInfo) EvaluateLifecycle(asOf time.Time) { + if s == nil { + return + } + plat := s.Platform + if plat == "" && s.CaseAttributes != nil && s.CaseAttributes["cloudProvider"] != "" { + plat = s.CaseAttributes["cloudProvider"] + " Virtual Edition" + } + lc := EvaluateLifecycle(s.Version, plat, asOf) + s.Lifecycle = &lc +} + +func isVirtualPlatform(upper string) bool { + return strings.Contains(upper, "Z100") || + strings.Contains(upper, "VIRTUAL") || + strings.Contains(upper, "VE-") || + strings.Contains(upper, "BIG-IP VE") || + upper == "VE" || + upper == "V100" || + strings.Contains(upper, "VMWARE") || + strings.Contains(upper, "KVM") || + strings.Contains(upper, "XEN") || + strings.Contains(upper, "HYPER-V") +} + +func isModernPlatform(upper string) bool { + // rSeries models: r2000, r4000, r5000, r10000, r2600, r2800, r4600, etc. + rSeriesRe := regexp.MustCompile(`(^|[^A-Z0-9])R\d{4}`) + if rSeriesRe.MatchString(upper) || strings.Contains(upper, "RSERIES") { + return true + } + // VELOS chassis and blades + if strings.Contains(upper, "VELOS") || strings.Contains(upper, "CX410") || strings.Contains(upper, "BX110") { + return true + } + return false +} + +func isISeriesMidrange(upper string) bool { + // Matches i2600, i2800, i4600, i4800, i5600, i5800, i7600, i7800, i10600, i10800, i11400, i11600, i11800 + iMidRe := regexp.MustCompile(`(^|[^A-Z0-9])I(2[68]00|4[68]00|5[68]00|7[68]00|10[68]00|11[468]00)([^A-Z0-9]|$)`) + if iMidRe.MatchString(upper) { + return true + } + return false +} + +func isISeriesHighEnd(upper string) bool { + return strings.Contains(upper, "I15600") || strings.Contains(upper, "I15800") +} + +func isVIPRIONMidrange(upper string) bool { + return strings.Contains(upper, "C2400") || strings.Contains(upper, "C2200") || + strings.Contains(upper, "B2150") || strings.Contains(upper, "B2250") +} + +func isVIPRIONHighEnd(upper string) bool { + return strings.Contains(upper, "C4800") || strings.Contains(upper, "C4480") || + strings.Contains(upper, "B4450") || strings.Contains(upper, "B4460") +} + +func isLegacyPlatform(upper string) bool { + // Matches legacy 2000, 4000, 5000, 7000, 10000, 12000 platforms (e.g. 2000s, 2200, 4200v, 5250v, 10200v) + legacyModelRe := regexp.MustCompile(`(^|[^A-Z0-9])(800|1600|3600|3900|6900|8900|11000|11050|2[02]00[SV]?|4[02]00[SV]?|5[02]00[SV]?|5250V?|7[02]00[SV]?|10[02]00[SV]?|10250V?|10350[VN]?|12[02]00[SV]?|12250V?)([^A-Z0-9]|$)`) + if legacyModelRe.MatchString(upper) { + return true + } + return false +} diff --git a/pkg/bigip/lifecycle_test.go b/pkg/bigip/lifecycle_test.go new file mode 100644 index 0000000..abcb3d2 --- /dev/null +++ b/pkg/bigip/lifecycle_test.go @@ -0,0 +1,444 @@ +package bigip + +import ( + "testing" + "time" +) + +func TestParseTMOSVersion(t *testing.T) { + tests := []struct { + input string + wantMaj int + wantMin int + wantPat int + wantOK bool + }{ + {"17.5.1", 17, 5, 1, true}, + {"17.5.1.9", 17, 5, 1, true}, + {"v16.1.4.1", 16, 1, 4, true}, + {"BIG-IP 15.1.10", 15, 1, 10, true}, + {"14.1.5", 14, 1, 5, true}, + {"13.1.3", 13, 1, 3, true}, + {"12.1.6", 12, 1, 6, true}, + {"11.6.5", 11, 6, 5, true}, + {"21.0.0", 21, 0, 0, true}, + {"invalid", 0, 0, 0, false}, + {"", 0, 0, 0, false}, + } + + for _, tt := range tests { + maj, min, pat, ok := ParseTMOSVersion(tt.input) + if ok != tt.wantOK { + t.Errorf("ParseTMOSVersion(%q) ok = %v; want %v", tt.input, ok, tt.wantOK) + continue + } + if ok { + if maj != tt.wantMaj || min != tt.wantMin || pat != tt.wantPat { + t.Errorf("ParseTMOSVersion(%q) = (%d, %d, %d); want (%d, %d, %d)", tt.input, maj, min, pat, tt.wantMaj, tt.wantMin, tt.wantPat) + } + } + } +} + +func TestEvaluateTMOSLifecycle(t *testing.T) { + now2026 := time.Date(2026, time.October, 7, 0, 0, 0, 0, time.UTC) + + tests := []struct { + name string + version string + asOf time.Time + wantStatus LifecycleStatus + wantEOL bool + }{ + { + name: "TMOS 11.6.5 is EOL", + version: "11.6.5", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 12.1.6 is EOL", + version: "12.1.6", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 13.1.5 is EOL", + version: "13.1.5", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 14.1.5 is EOL", + version: "14.1.5", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 15.0.0 is EOL", + version: "15.0.0", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 15.1.10 is EOL", + version: "15.1.10.2", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 16.0.0 is EOL", + version: "16.0.0", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 16.1.4 is EOL in late 2026", + version: "16.1.4.1", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 17.0.0 is EOL", + version: "17.0.0", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 17.1.1 is supported in 2026", + version: "17.1.1", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + }, + { + name: "TMOS 17.5.1.9 is supported in 2026", + version: "17.5.1.9", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + }, + { + name: "TMOS 17.1.1 approaches EOL in Feb 2027", + version: "17.1.1", + asOf: time.Date(2027, time.February, 1, 0, 0, 0, 0, time.UTC), + wantStatus: StatusApproachingEOL, + wantEOL: false, + }, + { + name: "TMOS 17.1.1 is EOL in April 2027", + version: "17.1.1", + asOf: time.Date(2027, time.April, 15, 0, 0, 0, 0, time.UTC), + wantStatus: StatusEOL, + wantEOL: true, + }, + { + name: "TMOS 21.0.0 is supported", + version: "21.0.0", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + }, + { + name: "Unknown empty version", + version: "", + asOf: now2026, + wantStatus: StatusUnknown, + wantEOL: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := EvaluateTMOSLifecycle(tt.version, tt.asOf) + if res.Status != tt.wantStatus { + t.Errorf("EvaluateTMOSLifecycle(%q) status = %v; want %v", tt.version, res.Status, tt.wantStatus) + } + if res.IsEOL != tt.wantEOL { + t.Errorf("EvaluateTMOSLifecycle(%q) isEOL = %v; want %v", tt.version, res.IsEOL, tt.wantEOL) + } + if tt.wantEOL && res.Warning == "" { + t.Errorf("expected warning for EOL version %q", tt.version) + } + }) + } +} + +func TestEvaluatePlatformLifecycle(t *testing.T) { + now2026 := time.Date(2026, time.October, 7, 0, 0, 0, 0, time.UTC) + + tests := []struct { + name string + platform string + asOf time.Time + wantStatus LifecycleStatus + wantEOL bool + wantVirtual bool + wantFamily string + }{ + { + name: "AWS Virtual Edition Z100", + platform: "Z100", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + wantVirtual: true, + wantFamily: "Virtual Edition", + }, + { + name: "Generic Virtual Edition", + platform: "BIG-IP Virtual Edition", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + wantVirtual: true, + wantFamily: "Virtual Edition", + }, + { + name: "rSeries r5800 appliance", + platform: "r5800", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + wantVirtual: false, + wantFamily: "F5 rSeries", + }, + { + name: "VELOS CX410 chassis", + platform: "CX410", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + wantVirtual: false, + wantFamily: "F5 VELOS", + }, + { + name: "iSeries i2800 in late 2026 (approaching EoSS)", + platform: "i2800", + asOf: now2026, + wantStatus: StatusApproachingEOL, + wantEOL: false, + wantVirtual: false, + wantFamily: "BIG-IP iSeries", + }, + { + name: "iSeries i2800 in 2027 (Extended Support)", + platform: "i2800", + asOf: time.Date(2027, time.March, 1, 0, 0, 0, 0, time.UTC), + wantStatus: StatusExtendedSupport, + wantEOL: false, + wantVirtual: false, + wantFamily: "BIG-IP iSeries", + }, + { + name: "iSeries C117 code (i850/i2600/i2800)", + platform: "BIG-IP C117", + asOf: now2026, + wantStatus: StatusApproachingEOL, + wantEOL: false, + wantVirtual: false, + wantFamily: "BIG-IP iSeries", + }, + { + name: "iSeries C119 code (i5600/i5800)", + platform: "C119", + asOf: now2026, + wantStatus: StatusApproachingEOL, + wantEOL: false, + wantVirtual: false, + wantFamily: "BIG-IP iSeries", + }, + { + name: "Legacy D110 code (7000s/7200v/BIG-IQ 7000)", + platform: "BIG-IP D110", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + wantVirtual: false, + wantFamily: "BIG-IP Legacy Hardware", + }, + { + name: "iSeries i15800 High-End", + platform: "i15800", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + wantVirtual: false, + wantFamily: "BIG-IP iSeries (High-End)", + }, + { + name: "VIPRION C2400 chassis in late 2026 (Extended Support)", + platform: "C2400", + asOf: now2026, + wantStatus: StatusExtendedSupport, + wantEOL: false, + wantVirtual: false, + wantFamily: "VIPRION", + }, + { + name: "VIPRION B2250 blade in late 2026 (Extended Support)", + platform: "B2250", + asOf: now2026, + wantStatus: StatusExtendedSupport, + wantEOL: false, + wantVirtual: false, + wantFamily: "VIPRION", + }, + { + name: "VIPRION C4800 chassis in late 2026", + platform: "C4800", + asOf: now2026, + wantStatus: StatusSupported, + wantEOL: false, + wantVirtual: false, + wantFamily: "VIPRION (High-End)", + }, + { + name: "VIPRION legacy blade B2100 is EOL", + platform: "B2100", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + wantVirtual: false, + wantFamily: "VIPRION (Legacy)", + }, + { + name: "Legacy 4200v appliance is EOL", + platform: "4200v", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + wantVirtual: false, + wantFamily: "BIG-IP Legacy Hardware", + }, + { + name: "Legacy 2000s hardware code C109 is EOL", + platform: "C109", + asOf: now2026, + wantStatus: StatusEOL, + wantEOL: true, + wantVirtual: false, + wantFamily: "BIG-IP Legacy Hardware", + }, + { + name: "Unknown platform", + platform: "CustomUnknownBox", + asOf: now2026, + wantStatus: StatusUnknown, + wantEOL: false, + wantVirtual: false, + wantFamily: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := EvaluatePlatformLifecycle(tt.platform, tt.asOf) + if res.Status != tt.wantStatus { + t.Errorf("EvaluatePlatformLifecycle(%q) status = %v; want %v", tt.platform, res.Status, tt.wantStatus) + } + if res.IsEOL != tt.wantEOL { + t.Errorf("EvaluatePlatformLifecycle(%q) isEOL = %v; want %v", tt.platform, res.IsEOL, tt.wantEOL) + } + if res.IsVirtual != tt.wantVirtual { + t.Errorf("EvaluatePlatformLifecycle(%q) isVirtual = %v; want %v", tt.platform, res.IsVirtual, tt.wantVirtual) + } + if res.Family != tt.wantFamily { + t.Errorf("EvaluatePlatformLifecycle(%q) family = %q; want %q", tt.platform, res.Family, tt.wantFamily) + } + }) + } +} + +func TestEvaluateLifecycle_Combined(t *testing.T) { + now2026 := time.Date(2026, time.October, 7, 0, 0, 0, 0, time.UTC) + + // Case 1: EOL Software + Approaching EOL Hardware + info := &SystemInfo{ + Version: "15.1.8", + Platform: "i2800", + } + info.EvaluateLifecycle(now2026) + + if info.Lifecycle == nil { + t.Fatal("expected Lifecycle to be populated") + } + if !info.Lifecycle.Software.IsEOL { + t.Errorf("expected software to be EOL, got %v", info.Lifecycle.Software.Status) + } + if info.Lifecycle.Hardware.Status != StatusApproachingEOL { + t.Errorf("expected hardware to be Approaching EOL, got %v", info.Lifecycle.Hardware.Status) + } + if len(info.Lifecycle.Warnings) != 2 { + t.Errorf("expected 2 warnings, got %d: %v", len(info.Lifecycle.Warnings), info.Lifecycle.Warnings) + } + + // Case 2: Supported Software on Virtual Edition + veInfo := &SystemInfo{ + Version: "17.5.1", + Platform: "Z100", + } + veInfo.EvaluateLifecycle(now2026) + if len(veInfo.Lifecycle.Warnings) != 0 { + t.Errorf("expected 0 warnings for 17.5.1 on Z100, got: %v", veInfo.Lifecycle.Warnings) + } +} + +func TestLookupPlatform(t *testing.T) { + tests := []struct { + input string + wantCode string + wantModel string + wantFamily string + wantVirtual bool + wantFound bool + }{ + {"C119", "C119", "BIG-IP i5600 / i5800", "BIG-IP iSeries", false, true}, + {"c119", "C119", "BIG-IP i5600 / i5800", "BIG-IP iSeries", false, true}, + {"C117", "C117", "BIG-IP i850 / i2600 / i2800", "BIG-IP iSeries", false, true}, + {"C115", "C115", "BIG-IP i4600 / i4800", "BIG-IP iSeries", false, true}, + {"C113", "C113", "BIG-IP 4000s / 4200v", "BIG-IP Legacy Hardware", false, true}, + {"D110", "D110", "BIG-IP 7000s / 7200v / BIG-IQ 7000", "BIG-IP Legacy Hardware", false, true}, + {"Z100", "Z100", "BIG-IP Virtual Edition", "Virtual Edition", true, true}, + {"Z100A", "Z100A", "BIG-IP Virtual Edition (AWS)", "Virtual Edition", true, true}, + {"F100", "F100", "VIPRION C2400 Chassis", "VIPRION", false, true}, + {"A108", "A108", "VIPRION B4300 Blade", "VIPRION (Legacy)", false, true}, + {"CX410", "CX410", "VELOS CX410 Chassis", "F5 VELOS", false, true}, + {"BX110", "BX110", "VELOS BX110 Blade", "F5 VELOS", false, true}, + {"platform=C119", "C119", "BIG-IP i5600 / i5800", "BIG-IP iSeries", false, true}, + {"BIG-IP i5800 (C119)", "C119", "BIG-IP i5600 / i5800", "BIG-IP iSeries", false, true}, + {"NonExistentPlatform", "", "", "", false, false}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + rec, found := LookupPlatform(tt.input) + if found != tt.wantFound { + t.Fatalf("LookupPlatform(%q) found = %v; want %v", tt.input, found, tt.wantFound) + } + if found { + if rec.Code != tt.wantCode { + t.Errorf("LookupPlatform(%q).Code = %q; want %q", tt.input, rec.Code, tt.wantCode) + } + if rec.Model != tt.wantModel { + t.Errorf("LookupPlatform(%q).Model = %q; want %q", tt.input, rec.Model, tt.wantModel) + } + if rec.Family != tt.wantFamily { + t.Errorf("LookupPlatform(%q).Family = %q; want %q", tt.input, rec.Family, tt.wantFamily) + } + if rec.IsVirtual != tt.wantVirtual { + t.Errorf("LookupPlatform(%q).IsVirtual = %v; want %v", tt.input, rec.IsVirtual, tt.wantVirtual) + } + } + }) + } +}