Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/myf5/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 47 additions & 0 deletions cmd/myf5/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"bytes"
"compress/gzip"
"context"
"io"
"os"
"path/filepath"
"strings"
"testing"

"myf5-cli/pkg/cases"
Expand Down Expand Up @@ -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)
}
}
64 changes: 64 additions & 0 deletions cmd/myf5/wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
44 changes: 44 additions & 0 deletions cmd/myf5/wizard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
72 changes: 64 additions & 8 deletions pkg/bigip/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"sort"
"strconv"
"strings"
"time"

"golang.org/x/crypto/ssh"

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -388,6 +434,7 @@ func InspectQKViewReader(r io.Reader) (*SystemInfo, error) {
}

info.PopulateCaseAttributes()
info.EvaluateLifecycle(time.Now())
return info, nil
}

Expand Down Expand Up @@ -464,6 +511,7 @@ func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) {
}

info.PopulateCaseAttributes()
info.EvaluateLifecycle(time.Now())
return info, nil
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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.]+)`)
Expand Down Expand Up @@ -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]
}
Expand Down
Loading
Loading