diff --git a/.gitignore b/.gitignore index 58f58ad..fc718ff 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,6 @@ id_ecdsa* .DS_Store .idea/ .vscode/ -.devcontainer/ scratch/ # Local scratch documentation & OpenAPI specifications diff --git a/pkg/bigip/failover.go b/pkg/bigip/failover.go index 88bcd42..0a2f751 100644 --- a/pkg/bigip/failover.go +++ b/pkg/bigip/failover.go @@ -191,7 +191,8 @@ func DownloadFileWithFailover( strings.HasSuffix(cleanRemote, ".iso") || strings.HasSuffix(cleanRemote, ".qkview") - tryRestFirst := isRestFriendly || opts.ClientCertPath != "" || (opts.IdentityFile == "" && (opts.Password != "" || os.Getenv("BIGIP_PASSWORD") != "" || os.Getenv("BIGIQ_PASSWORD") != "")) + hasRestAuth := opts.Password != "" || len(opts.PasswordBytes) > 0 || opts.ClientCertPath != "" || os.Getenv("BIGIP_PASSWORD") != "" || os.Getenv("BIGIQ_PASSWORD") != "" + tryRestFirst := hasRestAuth && (isRestFriendly || opts.IdentityFile == "") if tryRestFirst { savedPath, restErr := downloadViaIControl(ctx, host, remotePath, localDest, opts, progressCb) diff --git a/pkg/bigip/info.go b/pkg/bigip/info.go index f654168..9bea043 100644 --- a/pkg/bigip/info.go +++ b/pkg/bigip/info.go @@ -58,20 +58,42 @@ func (s *SystemInfo) PreferredSerialNumber() string { return s.RegistrationKey } +// IsBIGIQ returns true if the appliance is a BIG-IQ Centralized Management system. +func (s *SystemInfo) IsBIGIQ() bool { + if s == nil { + return false + } + if strings.Contains(strings.ToUpper(s.Product), "BIG-IQ") { + return true + } + if lvl, ok := s.ProvisionedModules["biq"]; ok && lvl != "" && lvl != "none" { + return true + } + return false +} + // PopulateCaseAttributes computes and stores standard attributes for F5 Support Case creation. func (s *SystemInfo) PopulateCaseAttributes() { if s == nil { return } + isBigIQ := s.IsBIGIQ() + family := "BIG-IP" + if isBigIQ { + family = "BIG-IQ" + } + attrs := map[string]string{ - "productFamily": "BIG-IP", + "productFamily": family, "serialNumber": s.PreferredSerialNumber(), "version": s.Version, "hostName": s.Hostname, } product := "BIG-IP" - if len(s.ProvisionedModules) > 0 { + if isBigIQ { + product = "BIG-IQ Centralized Management" + } else if len(s.ProvisionedModules) > 0 { var active []string for mod, level := range s.ProvisionedModules { if level != "none" && level != "" && !isInternalModule(mod) { @@ -133,8 +155,12 @@ func (s *SystemInfo) Summary() string { if 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) + model := rec.Model + if s.IsBIGIQ() && strings.HasPrefix(model, "BIG-IP Virtual Edition") { + model = strings.Replace(model, "BIG-IP Virtual Edition", "BIG-IQ Virtual Edition", 1) + } + if !strings.Contains(s.Platform, model) && s.Platform != model { + platDisplay = fmt.Sprintf("%s (%s)", s.Platform, model) } } b.WriteString(fmt.Sprintf(" Platform: %s\n", platDisplay)) diff --git a/pkg/bigip/info_test.go b/pkg/bigip/info_test.go index 653ef07..41bbff8 100644 --- a/pkg/bigip/info_test.go +++ b/pkg/bigip/info_test.go @@ -551,4 +551,30 @@ func TestSystemInfo_SummaryPlatformEnrichment(t *testing.T) { 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) } + + // Case 4: BIG-IQ 8.4.2.3 on Z100 Virtual Edition + info4 := &SystemInfo{ + Product: "BIG-IQ", + Version: "8.4.2.3", + Build: "0.0.8", + Edition: "Final", + Platform: "Z100", + ChassisSerialNumber: "test-bigiq-serial-001", + Hostname: "bigiq-ve.ec2.internal", + ProvisionedModules: map[string]string{"biq": "nominal"}, + } + info4.PopulateCaseAttributes() + sum4 := info4.Summary() + if info4.CaseAttributes["productFamily"] != "BIG-IQ" { + t.Errorf("CaseAttributes[productFamily] = %q; want BIG-IQ", info4.CaseAttributes["productFamily"]) + } + if info4.CaseAttributes["product"] != "BIG-IQ Centralized Management" { + t.Errorf("CaseAttributes[product] = %q; want BIG-IQ Centralized Management", info4.CaseAttributes["product"]) + } + if !strings.Contains(sum4, "Platform: Z100 (BIG-IQ Virtual Edition)") { + t.Errorf("Summary() did not enrich Z100 as BIG-IQ Virtual Edition:\n%s", sum4) + } + if !strings.Contains(sum4, "Software (8.4.2.3 ): Supported") || strings.Contains(sum4, "[WARNING]") { + t.Errorf("Summary() did not mark BIG-IQ 8.4.2.3 as Supported:\n%s", sum4) + } } diff --git a/pkg/bigip/lifecycle.go b/pkg/bigip/lifecycle.go index ae235b9..01decf5 100644 --- a/pkg/bigip/lifecycle.go +++ b/pkg/bigip/lifecycle.go @@ -583,6 +583,62 @@ func EvaluatePlatformLifecycle(platform string, asOf time.Time) HardwareLifecycl return res } +// EvaluateBIGIQLifecycle checks a BIG-IQ Centralized Management software version +// against F5 Software Lifecycle Policy K5903. +func EvaluateBIGIQLifecycle(version string, asOf time.Time) SoftwareLifecycle { + cleanVer := strings.TrimSpace(version) + res := SoftwareLifecycle{ + Version: cleanVer, + NoticeURL: URLPolicyTMOS, + Status: StatusUnknown, + } + + if cleanVer == "" { + return res + } + + major, minor, _, ok := ParseTMOSVersion(cleanVer) + if !ok { + return res + } + + res.Branch = fmt.Sprintf("%d.%d", major, minor) + + switch { + case major < 8 || (major == 8 && minor < 3): + res.EoSD = "2024-12-31" + res.EoTS = "2024-12-31" + res.Status = StatusEOL + res.IsEOL = true + res.Warning = fmt.Sprintf("BIG-IQ version %s reached End of Life (EoSD/EoTS) on %s (K5903).", cleanVer, res.EoSD) + res.Recommendation = "Upgrade to an actively supported BIG-IQ 8.4.x release to receive software fixes and support." + return res + + case major == 8 && minor == 3: + eosdDate := time.Date(2025, time.December, 31, 23, 59, 59, 0, time.UTC) + res.EoSD = eosdDate.Format("2006-01-02") + res.EoTS = res.EoSD + if asOf.After(eosdDate) { + res.Status = StatusEOL + res.IsEOL = true + res.Warning = fmt.Sprintf("BIG-IQ version %s reached End of Life (EoSD/EoTS) on %s (K5903).", cleanVer, res.EoSD) + res.Recommendation = "Upgrade to an actively supported BIG-IQ 8.4.x release to receive software fixes and support." + } else { + res.Status = StatusSupported + res.IsEOL = false + res.Recommendation = fmt.Sprintf("Standard support active until %s.", res.EoSD) + } + return res + + default: + // BIG-IQ 8.4.x and newer releases + res.Status = StatusSupported + res.IsEOL = false + res.Recommendation = "Active BIG-IQ release stream (K5903)." + return res + } +} + // EvaluateLifecycle evaluates both software and hardware lifecycle statuses. func EvaluateLifecycle(version, platform string, asOf time.Time) LifecycleInfo { sw := EvaluateTMOSLifecycle(version, asOf) @@ -612,7 +668,25 @@ func (s *SystemInfo) EvaluateLifecycle(asOf time.Time) { if plat == "" && s.CaseAttributes != nil && s.CaseAttributes["cloudProvider"] != "" { plat = s.CaseAttributes["cloudProvider"] + " Virtual Edition" } - lc := EvaluateLifecycle(s.Version, plat, asOf) + var lc LifecycleInfo + if s.IsBIGIQ() { + sw := EvaluateBIGIQLifecycle(s.Version, asOf) + hw := EvaluatePlatformLifecycle(plat, asOf) + var warnings []string + if sw.Warning != "" { + warnings = append(warnings, sw.Warning) + } + if hw.Warning != "" { + warnings = append(warnings, hw.Warning) + } + lc = LifecycleInfo{ + Software: sw, + Hardware: hw, + Warnings: warnings, + } + } else { + lc = EvaluateLifecycle(s.Version, plat, asOf) + } s.Lifecycle = &lc } diff --git a/pkg/bigip/scp.go b/pkg/bigip/scp.go index 3facf7d..eeae82b 100644 --- a/pkg/bigip/scp.go +++ b/pkg/bigip/scp.go @@ -225,10 +225,16 @@ func SCPUpload(ctx context.Context, localPath string, destDir string, opts SCPOp } else { destDir = "/var/tmp" } + } else if path.Base(destDir) == baseFilename || path.Ext(destDir) != "" { + baseFilename = path.Base(destDir) + destDir = path.Dir(destDir) } if err := ValidateRemoteAppliancePath(destDir); err != nil { return "", fmt.Errorf("invalid destination directory: %w", err) } + if err := ValidateRemoteAppliancePath(path.Join(destDir, baseFilename)); err != nil { + return "", fmt.Errorf("invalid destination file path: %w", err) + } host, port := CleanSSHHost(opts.Host) if opts.Port > 0 {