From b5c2746ef96d9e8b6d180328567f9dcf99699e63 Mon Sep 17 00:00:00 2001 From: Greg Robinson Date: Wed, 7 Oct 2026 01:23:14 -0400 Subject: [PATCH 1/3] feat(bigip): warn users on EOL hardware and software releases - Implement TMOS software lifecycle evaluation based on F5 K5903 (EoSD/EoTS) - Implement hardware platform lifecycle evaluation based on F5 K4309 (EoS/EoNSS/EoSS/EoTS) - Integrate lifecycle evaluations and warning banners in: - Appliance system info inspection (iControl REST, SSH, QKView) - Case creation pre-flight CLI validation ('myf5 cases create') - Support Case Wizard ('myf5 cases wizard') across appliance/QKView extraction, version input, and review confirmation - Add unit tests for lifecycle evaluations, CLI warnings, and wizard flows --- cmd/myf5/main.go | 17 ++ cmd/myf5/main_test.go | 47 ++++ cmd/myf5/wizard.go | 70 ++++++ cmd/myf5/wizard_test.go | 44 ++++ pkg/bigip/info.go | 34 +++ pkg/bigip/info_test.go | 45 ++++ pkg/bigip/lifecycle.go | 469 ++++++++++++++++++++++++++++++++++++ pkg/bigip/lifecycle_test.go | 376 +++++++++++++++++++++++++++++ 8 files changed, 1102 insertions(+) create mode 100644 pkg/bigip/lifecycle.go create mode 100644 pkg/bigip/lifecycle_test.go diff --git a/cmd/myf5/main.go b/cmd/myf5/main.go index b4bed06..225aab1 100644 --- a/cmd/myf5/main.go +++ b/cmd/myf5/main.go @@ -1648,6 +1648,23 @@ 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 lc.Hardware.Recommendation != "" && lc.Hardware.Warning != "" { + fmt.Fprintf(os.Stderr, " Hardware Rec: %s\n", lc.Hardware.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..3624eda 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 Hardware Rec in stderr, got: %q", errOutput) + } +} diff --git a/cmd/myf5/wizard.go b/cmd/myf5/wizard.go index 4a8ce6c..aa91735 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,21 @@ 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) + } + if info.Lifecycle.Hardware.Recommendation != "" && info.Lifecycle.Hardware.Warning != "" { + fmt.Fprintf(out, " - Hardware Rec: %s\n", info.Lifecycle.Hardware.Recommendation) + } + } fmt.Fprintln(out) } } @@ -611,6 +639,21 @@ 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) + } + if info.Lifecycle.Hardware.Recommendation != "" && info.Lifecycle.Hardware.Warning != "" { + fmt.Fprintf(out, " - Hardware Rec: %s\n", info.Lifecycle.Hardware.Recommendation) + } + } fmt.Fprintln(out) } } @@ -658,6 +701,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 +1008,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..936d66a 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 { @@ -157,6 +159,35 @@ 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)) + } + if s.Lifecycle.Hardware.Recommendation != "" && s.Lifecycle.Hardware.Warning != "" { + b.WriteString(fmt.Sprintf(" Hardware Rec: %s\n", s.Lifecycle.Hardware.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 (%s)\n", fam, s.Lifecycle.Hardware.Status, s.Lifecycle.Hardware.Recommendation)) + } + } + } return b.String() } @@ -388,6 +419,7 @@ func InspectQKViewReader(r io.Reader) (*SystemInfo, error) { } info.PopulateCaseAttributes() + info.EvaluateLifecycle(time.Now()) return info, nil } @@ -464,6 +496,7 @@ func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) { } info.PopulateCaseAttributes() + info.EvaluateLifecycle(time.Now()) return info, nil } @@ -549,6 +582,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 } diff --git a/pkg/bigip/info_test.go b/pkg/bigip/info_test.go index 045e7f3..35a02f5 100644 --- a/pkg/bigip/info_test.go +++ b/pkg/bigip/info_test.go @@ -446,3 +446,48 @@ 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 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) + } +} diff --git a/pkg/bigip/lifecycle.go b/pkg/bigip/lifecycle.go new file mode 100644 index 0000000..b17452b --- /dev/null +++ b/pkg/bigip/lifecycle.go @@ -0,0 +1,469 @@ +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 reference URLs +const ( + URLPolicyTMOS = "https://my.f5.com/manage/s/article/K5903" + URLPolicyHardware = "https://my.f5.com/manage/s/article/K4309" +) + +// 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 against F5 Hardware Product Lifecycle Support Policy K4309. +func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycle { + cleanPlat := strings.TrimSpace(platform) + upper := strings.ToUpper(cleanPlat) + + res := HardwareLifecycle{ + Platform: cleanPlat, + NoticeURL: URLPolicyHardware, + Status: StatusUnknown, + } + + if cleanPlat == "" { + res.Status = StatusUnknown + return res + } + + // 1. Virtual Edition (Cloud, Hypervisors) + if isVirtualPlatform(upper) { + res.Family = "Virtual Edition" + res.IsVirtual = true + res.Status = StatusSupported + res.Recommendation = "Virtual appliance; lifecycle is governed by the TMOS software version (K5903)." + return res + } + + // 2. 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 + res.Recommendation = "Current generation architecture running F5OS." + return res + } + + // 3. 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) + res.Recommendation = "Migrate to F5 rSeries appliances." + } 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) + res.Recommendation = "Plan migration to F5 rSeries appliances before EoSS." + } else { + res.Status = StatusSupported + res.Recommendation = "Platform in Regular Support until 2027-01-01." + } + return res + } + + // 4. 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 + res.Recommendation = "Regular support active until 2029-01-01." + } + return res + } + + // 5. 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) + res.Recommendation = "Migrate chassis workloads to F5 VELOS or rSeries systems." + } else { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("VIPRION platform %s approaches End of Software Support on 2026-04-01 (K4309).", cleanPlat) + res.Recommendation = "Plan migration to F5 VELOS." + } + return res + } + + // 6. 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) + res.Recommendation = "Migrate to F5 VELOS." + } 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) + res.Recommendation = "Supported until 2029-01-01; evaluate migration to F5 VELOS." + } else { + res.Status = StatusSupported + res.Recommendation = "Regular support active until 2029-01-01." + } + return res + } + + // 7. VIPRION Legacy Blades: B2100, B4300 + if strings.Contains(upper, "B2100") || strings.Contains(upper, "B4300") { + 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) + res.Recommendation = "Replace blade with supported hardware." + return res + } + + // 8. Legacy Hardware (2000s, 4000s, 5000s, 7000s, 10000s, 12000s, C109, 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) + res.Recommendation = "Upgrade to active F5 rSeries appliances." + 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 + } + // Hardware motherboard codes: D110 (i2800), D111 (i4800), D112 (i5800), D113 (i7800), etc. + codes := []string{"D110", "D111", "D112", "D113", "D114", "D116", "D118"} + for _, code := range codes { + if strings.Contains(upper, code) { + 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])(2[02]00[SV]?|4[02]00[SV]?|5[02]00[SV]?|5250V?|7[02]00[SV]?|10[02]00[SV]?|10250V?|12[02]00[SV]?|12250V?)([^A-Z0-9]|$)`) + if legacyModelRe.MatchString(upper) { + return true + } + // Legacy hardware codes (C109 for 2000s, C112 for 4000, C113 for 4200v, A109 for 10200v, etc.) + legacyCodes := []string{"C109", "C112", "C113", "C114", "C115", "A109", "A112", "A113", "D106"} + for _, code := range legacyCodes { + if strings.Contains(upper, code) { + return true + } + } + return false +} diff --git a/pkg/bigip/lifecycle_test.go b/pkg/bigip/lifecycle_test.go new file mode 100644 index 0000000..b9e57e0 --- /dev/null +++ b/pkg/bigip/lifecycle_test.go @@ -0,0 +1,376 @@ +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 D110 code (i2800)", + platform: "BIG-IP D110", + asOf: now2026, + wantStatus: StatusApproachingEOL, + wantEOL: false, + wantVirtual: false, + wantFamily: "BIG-IP iSeries", + }, + { + 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) + } +} From cf2ee348601532b0655dff4fd623383db83b6cb6 Mon Sep 17 00:00:00 2001 From: Greg Robinson Date: Wed, 7 Oct 2026 03:38:16 -0400 Subject: [PATCH 2/3] feat(bigip): map hardware platform IDs from K13144 --- pkg/bigip/info.go | 41 ++++- pkg/bigip/info_test.go | 61 ++++++++ pkg/bigip/lifecycle.go | 290 ++++++++++++++++++++++++++++++++---- pkg/bigip/lifecycle_test.go | 72 ++++++++- 4 files changed, 425 insertions(+), 39 deletions(-) diff --git a/pkg/bigip/info.go b/pkg/bigip/info.go index 936d66a..136a368 100644 --- a/pkg/bigip/info.go +++ b/pkg/bigip/info.go @@ -87,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 @@ -119,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)) @@ -703,7 +721,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.]+)`) @@ -750,6 +769,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 35a02f5..e202311 100644 --- a/pkg/bigip/info_test.go +++ b/pkg/bigip/info_test.go @@ -491,3 +491,64 @@ func TestSystemInfo_SummaryLifecycle(t *testing.T) { 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 index b17452b..41e89b3 100644 --- a/pkg/bigip/lifecycle.go +++ b/pkg/bigip/lifecycle.go @@ -19,12 +19,135 @@ const ( StatusUnknown LifecycleStatus = "Unknown" ) -// Policy reference URLs +// 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"` @@ -210,9 +333,17 @@ func EvaluateTMOSLifecycle(version string, asOf time.Time) SoftwareLifecycle { return res } -// EvaluatePlatformLifecycle checks a hardware platform name against F5 Hardware Product Lifecycle Support Policy K4309. +// 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{ @@ -221,12 +352,127 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl Status: StatusUnknown, } - if cleanPlat == "" { - res.Status = StatusUnknown - return res + // 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 + res.Recommendation = "Virtual appliance; lifecycle is governed by the TMOS software version (K5903)." + return res + + case CategoryModern: + res.Status = StatusSupported + res.Recommendation = "Current generation architecture running F5OS." + 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) + res.Recommendation = "Migrate to F5 rSeries appliances." + } 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) + res.Recommendation = "Plan migration to F5 rSeries appliances before EoSS." + } else { + res.Status = StatusSupported + res.Recommendation = "Platform in Regular Support until 2027-01-01." + } + 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 + res.Recommendation = "Regular support active until 2029-01-01." + } + 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) + res.Recommendation = "Migrate chassis workloads to F5 VELOS or rSeries systems." + } else { + res.Status = StatusApproachingEOL + res.Warning = fmt.Sprintf("VIPRION platform %s approaches End of Software Support on 2026-04-01 (K4309).", displayPlat) + res.Recommendation = "Plan migration to F5 VELOS." + } + 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) + res.Recommendation = "Migrate to F5 VELOS." + } 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) + res.Recommendation = "Supported until 2029-01-01; evaluate migration to F5 VELOS." + } else { + res.Status = StatusSupported + res.Recommendation = "Regular support active until 2029-01-01." + } + 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) + res.Recommendation = "Replace blade/chassis with supported hardware." + 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) + res.Recommendation = "Upgrade to active F5 rSeries appliances." + return res + } } - // 1. Virtual Edition (Cloud, Hypervisors) + // 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 @@ -235,7 +481,7 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } - // 2. Modern Platforms: rSeries & VELOS + // Modern Platforms: rSeries & VELOS if isModernPlatform(upper) { if strings.Contains(upper, "CX") || strings.Contains(upper, "BX") || strings.Contains(upper, "VELOS") { res.Family = "F5 VELOS" @@ -247,7 +493,7 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } - // 3. iSeries Midrange: i2600, i2800, i4600, i4800, i5600, i5800, i7600, i7800, i10600, i10800, i11400, i11600, i11800 + // 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" @@ -273,7 +519,7 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } - // 4. iSeries High-End: i15600, i15800 + // iSeries High-End: i15600, i15800 if isISeriesHighEnd(upper) { res.Family = "BIG-IP iSeries (High-End)" res.EoS = "2027-01-01" @@ -292,7 +538,7 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } - // 5. VIPRION Midrange: C2400, C2200, B2150, B2250 + // VIPRION Midrange: C2400, C2200, B2150, B2250 if isVIPRIONMidrange(upper) { res.Family = "VIPRION" res.EoS = "2023-04-01" @@ -313,7 +559,7 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } - // 6. VIPRION High-End: C4800, C4480, B4450, B4460 + // VIPRION High-End: C4800, C4480, B4450, B4460 if isVIPRIONHighEnd(upper) { res.Family = "VIPRION (High-End)" res.EoS = "2026-01-01" @@ -339,8 +585,8 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } - // 7. VIPRION Legacy Blades: B2100, B4300 - if strings.Contains(upper, "B2100") || strings.Contains(upper, "B4300") { + // 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 @@ -349,7 +595,7 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } - // 8. Legacy Hardware (2000s, 4000s, 5000s, 7000s, 10000s, 12000s, C109, etc.) + // Legacy Hardware (2000s, 4000s, 5000s, 7000s, 10000s, 12000s, etc.) if isLegacyPlatform(upper) { res.Family = "BIG-IP Legacy Hardware" res.Status = StatusEOL @@ -428,13 +674,6 @@ func isISeriesMidrange(upper string) bool { if iMidRe.MatchString(upper) { return true } - // Hardware motherboard codes: D110 (i2800), D111 (i4800), D112 (i5800), D113 (i7800), etc. - codes := []string{"D110", "D111", "D112", "D113", "D114", "D116", "D118"} - for _, code := range codes { - if strings.Contains(upper, code) { - return true - } - } return false } @@ -454,16 +693,9 @@ func isVIPRIONHighEnd(upper string) bool { 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])(2[02]00[SV]?|4[02]00[SV]?|5[02]00[SV]?|5250V?|7[02]00[SV]?|10[02]00[SV]?|10250V?|12[02]00[SV]?|12250V?)([^A-Z0-9]|$)`) + 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 } - // Legacy hardware codes (C109 for 2000s, C112 for 4000, C113 for 4200v, A109 for 10200v, etc.) - legacyCodes := []string{"C109", "C112", "C113", "C114", "C115", "A109", "A112", "A113", "D106"} - for _, code := range legacyCodes { - if strings.Contains(upper, code) { - return true - } - } return false } diff --git a/pkg/bigip/lifecycle_test.go b/pkg/bigip/lifecycle_test.go index b9e57e0..abcb3d2 100644 --- a/pkg/bigip/lifecycle_test.go +++ b/pkg/bigip/lifecycle_test.go @@ -240,14 +240,32 @@ func TestEvaluatePlatformLifecycle(t *testing.T) { wantFamily: "BIG-IP iSeries", }, { - name: "iSeries D110 code (i2800)", - platform: "BIG-IP D110", + 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", @@ -374,3 +392,53 @@ func TestEvaluateLifecycle_Combined(t *testing.T) { 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) + } + } + }) + } +} From 2fa58468bb37965636ca975e0c21627327cdd67a Mon Sep 17 00:00:00 2001 From: Greg Robinson Date: Wed, 7 Oct 2026 04:02:57 -0400 Subject: [PATCH 3/3] fix(bigip): suppress hardware migration recommendations per user preference --- cmd/myf5/main.go | 3 --- cmd/myf5/main_test.go | 4 ++-- cmd/myf5/wizard.go | 6 ------ pkg/bigip/info.go | 5 +---- pkg/bigip/info_test.go | 4 ++-- pkg/bigip/lifecycle.go | 26 -------------------------- 6 files changed, 5 insertions(+), 43 deletions(-) diff --git a/cmd/myf5/main.go b/cmd/myf5/main.go index 225aab1..c816dbc 100644 --- a/cmd/myf5/main.go +++ b/cmd/myf5/main.go @@ -1660,9 +1660,6 @@ func handleCaseCreate(ctx context.Context, client *cases.Client, args []string) if lc.Software.Recommendation != "" && lc.Software.Warning != "" { fmt.Fprintf(os.Stderr, " Software Rec: %s\n", lc.Software.Recommendation) } - if lc.Hardware.Recommendation != "" && lc.Hardware.Warning != "" { - fmt.Fprintf(os.Stderr, " Hardware Rec: %s\n", lc.Hardware.Recommendation) - } } if dryRun { diff --git a/cmd/myf5/main_test.go b/cmd/myf5/main_test.go index 3624eda..ab2b764 100644 --- a/cmd/myf5/main_test.go +++ b/cmd/myf5/main_test.go @@ -251,7 +251,7 @@ func TestCaseCreate_LifecycleWarnings(t *testing.T) { 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 Hardware 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 aa91735..6b6a820 100644 --- a/cmd/myf5/wizard.go +++ b/cmd/myf5/wizard.go @@ -604,9 +604,6 @@ func runStep1AutoDetection(ctx context.Context, reader *bufio.Reader, out io.Wri if info.Lifecycle.Software.Recommendation != "" && info.Lifecycle.Software.Warning != "" { fmt.Fprintf(out, " - Software Rec: %s\n", info.Lifecycle.Software.Recommendation) } - if info.Lifecycle.Hardware.Recommendation != "" && info.Lifecycle.Hardware.Warning != "" { - fmt.Fprintf(out, " - Hardware Rec: %s\n", info.Lifecycle.Hardware.Recommendation) - } } fmt.Fprintln(out) } @@ -650,9 +647,6 @@ func runStep1AutoDetection(ctx context.Context, reader *bufio.Reader, out io.Wri if info.Lifecycle.Software.Recommendation != "" && info.Lifecycle.Software.Warning != "" { fmt.Fprintf(out, " - Software Rec: %s\n", info.Lifecycle.Software.Recommendation) } - if info.Lifecycle.Hardware.Recommendation != "" && info.Lifecycle.Hardware.Warning != "" { - fmt.Fprintf(out, " - Hardware Rec: %s\n", info.Lifecycle.Hardware.Recommendation) - } } fmt.Fprintln(out) } diff --git a/pkg/bigip/info.go b/pkg/bigip/info.go index 136a368..f654168 100644 --- a/pkg/bigip/info.go +++ b/pkg/bigip/info.go @@ -189,9 +189,6 @@ func (s *SystemInfo) Summary() string { if s.Lifecycle.Software.Recommendation != "" && s.Lifecycle.Software.Warning != "" { b.WriteString(fmt.Sprintf(" Software Rec: %s\n", s.Lifecycle.Software.Recommendation)) } - if s.Lifecycle.Hardware.Recommendation != "" && s.Lifecycle.Hardware.Warning != "" { - b.WriteString(fmt.Sprintf(" Hardware Rec: %s\n", s.Lifecycle.Hardware.Recommendation)) - } } else { b.WriteString("\nLifecycle & Support Status:\n") if s.Lifecycle.Software.Status != StatusUnknown && s.Lifecycle.Software.Status != "" { @@ -202,7 +199,7 @@ func (s *SystemInfo) Summary() string { if fam == "" { fam = s.Lifecycle.Hardware.Platform } - b.WriteString(fmt.Sprintf(" Hardware (%-9s): %s (%s)\n", fam, s.Lifecycle.Hardware.Status, s.Lifecycle.Hardware.Recommendation)) + b.WriteString(fmt.Sprintf(" Hardware (%-9s): %s\n", fam, s.Lifecycle.Hardware.Status)) } } } diff --git a/pkg/bigip/info_test.go b/pkg/bigip/info_test.go index e202311..653ef07 100644 --- a/pkg/bigip/info_test.go +++ b/pkg/bigip/info_test.go @@ -467,8 +467,8 @@ func TestSystemInfo_SummaryLifecycle(t *testing.T) { 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 Hardware 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 diff --git a/pkg/bigip/lifecycle.go b/pkg/bigip/lifecycle.go index 41e89b3..ae235b9 100644 --- a/pkg/bigip/lifecycle.go +++ b/pkg/bigip/lifecycle.go @@ -366,12 +366,10 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl switch rec.Category { case CategoryVirtualEdition: res.Status = StatusSupported - res.Recommendation = "Virtual appliance; lifecycle is governed by the TMOS software version (K5903)." return res case CategoryModern: res.Status = StatusSupported - res.Recommendation = "Current generation architecture running F5OS." return res case CategoryISeriesMidrange: @@ -386,14 +384,11 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl 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) - res.Recommendation = "Migrate to F5 rSeries appliances." } 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) - res.Recommendation = "Plan migration to F5 rSeries appliances before EoSS." } else { res.Status = StatusSupported - res.Recommendation = "Platform in Regular Support until 2027-01-01." } return res @@ -409,7 +404,6 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl res.Warning = fmt.Sprintf("Platform %s reached End of Sale on 2027-01-01 (K4309).", displayPlat) } else { res.Status = StatusSupported - res.Recommendation = "Regular support active until 2029-01-01." } return res @@ -423,11 +417,9 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl 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) - res.Recommendation = "Migrate chassis workloads to F5 VELOS or rSeries systems." } else { res.Status = StatusApproachingEOL res.Warning = fmt.Sprintf("VIPRION platform %s approaches End of Software Support on 2026-04-01 (K4309).", displayPlat) - res.Recommendation = "Plan migration to F5 VELOS." } return res @@ -443,14 +435,11 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl if asOf.After(eossDate) { res.Status = StatusExtendedSupport res.Warning = fmt.Sprintf("VIPRION platform %s reached EoSS on 2029-01-01 (K4309).", displayPlat) - res.Recommendation = "Migrate to F5 VELOS." } 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) - res.Recommendation = "Supported until 2029-01-01; evaluate migration to F5 VELOS." } else { res.Status = StatusSupported - res.Recommendation = "Regular support active until 2029-01-01." } return res @@ -458,14 +447,12 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl res.Status = StatusEOL res.IsEOL = true res.Warning = fmt.Sprintf("VIPRION platform %s reached End of Life (past End of Technical Support, K4309).", displayPlat) - res.Recommendation = "Replace blade/chassis with supported hardware." 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) - res.Recommendation = "Upgrade to active F5 rSeries appliances." return res } } @@ -477,7 +464,6 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl res.Family = "Virtual Edition" res.IsVirtual = true res.Status = StatusSupported - res.Recommendation = "Virtual appliance; lifecycle is governed by the TMOS software version (K5903)." return res } @@ -489,7 +475,6 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl res.Family = "F5 rSeries" } res.Status = StatusSupported - res.Recommendation = "Current generation architecture running F5OS." return res } @@ -507,14 +492,11 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl 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) - res.Recommendation = "Migrate to F5 rSeries appliances." } 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) - res.Recommendation = "Plan migration to F5 rSeries appliances before EoSS." } else { res.Status = StatusSupported - res.Recommendation = "Platform in Regular Support until 2027-01-01." } return res } @@ -533,7 +515,6 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl res.Warning = fmt.Sprintf("Platform %s reached End of Sale on 2027-01-01 (K4309).", cleanPlat) } else { res.Status = StatusSupported - res.Recommendation = "Regular support active until 2029-01-01." } return res } @@ -550,11 +531,9 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl 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) - res.Recommendation = "Migrate chassis workloads to F5 VELOS or rSeries systems." } else { res.Status = StatusApproachingEOL res.Warning = fmt.Sprintf("VIPRION platform %s approaches End of Software Support on 2026-04-01 (K4309).", cleanPlat) - res.Recommendation = "Plan migration to F5 VELOS." } return res } @@ -573,14 +552,11 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl if asOf.After(eossDate) { res.Status = StatusExtendedSupport res.Warning = fmt.Sprintf("VIPRION platform %s reached EoSS on 2029-01-01 (K4309).", cleanPlat) - res.Recommendation = "Migrate to F5 VELOS." } 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) - res.Recommendation = "Supported until 2029-01-01; evaluate migration to F5 VELOS." } else { res.Status = StatusSupported - res.Recommendation = "Regular support active until 2029-01-01." } return res } @@ -591,7 +567,6 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl res.Status = StatusEOL res.IsEOL = true res.Warning = fmt.Sprintf("VIPRION blade %s reached End of Life (past End of Technical Support, K4309).", cleanPlat) - res.Recommendation = "Replace blade with supported hardware." return res } @@ -601,7 +576,6 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl 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) - res.Recommendation = "Upgrade to active F5 rSeries appliances." return res }