From 89df2f7e01708f5cca6a9082076b219cf52a7a0e Mon Sep 17 00:00:00 2001 From: Greg Robinson Date: Sat, 7 Nov 2026 01:09:25 -0500 Subject: [PATCH 1/2] feat(appliance): autodetect BIG-IP vs BIG-IQ and fix multi-step password zeroing --- cmd/myf5/main.go | 587 ++++++++++++++++++++----------------- pkg/bigip/client.go | 52 +++- pkg/bigip/client_test.go | 88 +++++- pkg/bigip/failover.go | 115 +++----- pkg/bigip/failover_test.go | 29 ++ pkg/bigip/geoip.go | 27 +- pkg/bigip/info.go | 8 + pkg/bigip/info_test.go | 12 + 8 files changed, 552 insertions(+), 366 deletions(-) diff --git a/cmd/myf5/main.go b/cmd/myf5/main.go index 23a7e24..87d9ae7 100644 --- a/cmd/myf5/main.go +++ b/cmd/myf5/main.go @@ -131,13 +131,11 @@ func executeSubcommand(ctx context.Context, subcommand string, args []string) { handleCases(ctx, append([]string{"wizard"}, args...)) case "ihealth": handleIHealth(ctx, args) - case "bigip": - handleBIGIP(ctx, args) - case "bigiq": - handleBIGIQ(ctx, args) - case "geoip": - handleBIGIP(ctx, append([]string{"geoip"}, args...)) - case "push": + case "bigip", "bigiq", "appliance", "device": + handleAppliance(ctx, subcommand, args) + case "ready", "info", "sysinfo", "system-info", "serial", "sn", "serialnumber", "qkview", "geoip": + handleAppliance(ctx, "auto", append([]string{subcommand}, args...)) + case "push", "upload": handlePush(ctx, args) case "pull", "fetch": handlePull(ctx, args) @@ -1626,6 +1624,8 @@ func handleCaseCreate(ctx context.Context, client *cases.Client, args []string) payload := map[string]interface{}{} dryRun := false + var fromAppliance string + var fromTargetKind string flagToField := map[string]string{ "--family": "productFamily", @@ -1646,6 +1646,21 @@ func handleCaseCreate(ctx context.Context, client *cases.Client, args []string) for i := 0; i < len(args); i++ { a := args[i] + if a == "--from-appliance" || a == "--from-device" || a == "--from-bigiq" || a == "--from-bigip" { + if i+1 >= len(args) { + fmt.Fprintf(os.Stderr, "Flag %s requires a value (host or IP)\n", a) + exitFunc(1) + } + fromAppliance = args[i+1] + fromTargetKind = "auto" + if a == "--from-bigiq" { + fromTargetKind = "BIG-IQ" + } else if a == "--from-bigip" { + fromTargetKind = "BIG-IP" + } + i++ + continue + } if a == "--wizard" || a == "-w" { if err := RunCaseWizard(ctx, os.Stdin, os.Stdout, client); err != nil { fmt.Fprintf(os.Stderr, "[ERROR] %v\n", err) @@ -1679,6 +1694,26 @@ func handleCaseCreate(ctx context.Context, client *cases.Client, args []string) payload["preferredContactMethod"] = "Email" } + if fromAppliance != "" { + spinner := progress.NewSpinner(fmt.Sprintf("Inspecting appliance %s...", fromAppliance)) + spinner.Start() + + creds := resolveApplianceCreds(fromAppliance, fromTargetKind, "", "", "", "", false) + sysInfo := getSystemInfoTarget(ctx, fromAppliance, creds.TargetKind, creds.User, 0, creds.Identity, "auto", creds.Cert, creds.Key) + if sysInfo != nil { + sysInfo.PopulateCaseAttributes() + spinner.Success(fmt.Sprintf("Autodetected %s %s (Serial: %s)", sysInfo.ApplianceKind(), sysInfo.Version, sysInfo.PreferredSerialNumber())) + for k, v := range sysInfo.CaseAttributes { + if _, exists := payload[k]; !exists && v != "" { + payload[k] = v + } + } + } else { + spinner.Fail("Failed to extract appliance info") + exitFunc(1) + } + } + // Warn if BIG-IP TMOS or hardware platform is EOL or approaching EOL family, _ := payload["productFamily"].(string) ver, _ := payload["version"].(string) @@ -1849,6 +1884,102 @@ func handleIHealth(ctx context.Context, args []string) { } // ---------------------------------------------------------------------------- +type ApplianceCreds struct { + User string + Password string + Cert string + Key string + Identity string + TargetKind string +} + +func resolveApplianceCreds(host, targetKind, flagUser, flagCert, flagKey, flagIdentity string, isSCP bool) ApplianceCreds { + cleanHost := strings.TrimSpace(host) + if _, h, _, _, err := bigip.ParseAndValidateApplianceHost(cleanHost, 443); err == nil && h != "" { + cleanHost = h + } + + // Host-matching awareness + prefix1 := "BIGIP" + prefix2 := "BIGIQ" + + if os.Getenv("BIGIQ_HOST") != "" && (strings.EqualFold(host, os.Getenv("BIGIQ_HOST")) || strings.EqualFold(cleanHost, os.Getenv("BIGIQ_HOST"))) { + prefix1, prefix2 = "BIGIQ", "BIGIP" + targetKind = "BIG-IQ" + } else if os.Getenv("BIGIP_HOST") != "" && (strings.EqualFold(host, os.Getenv("BIGIP_HOST")) || strings.EqualFold(cleanHost, os.Getenv("BIGIP_HOST"))) { + prefix1, prefix2 = "BIGIP", "BIGIQ" + targetKind = "BIG-IP" + } else if strings.EqualFold(targetKind, "BIG-IQ") || strings.EqualFold(targetKind, "bigiq") { + prefix1, prefix2 = "BIGIQ", "BIGIP" + targetKind = "BIG-IQ" + } else if strings.EqualFold(targetKind, "BIG-IP") || strings.EqualFold(targetKind, "bigip") { + prefix1, prefix2 = "BIGIP", "BIGIQ" + targetKind = "BIG-IP" + } else { + prefix1, prefix2 = "BIGIP", "BIGIQ" + targetKind = "auto" + } + + creds := ApplianceCreds{TargetKind: targetKind} + + // Username + if flagUser != "" { + creds.User = flagUser + } else if u := os.Getenv(prefix1 + "_USER"); u != "" { + creds.User = u + } else if u := os.Getenv(prefix2 + "_USER"); u != "" { + creds.User = u + } else if u := os.Getenv("APPLIANCE_USER"); u != "" { + creds.User = u + } else { + if isSCP { + creds.User = "root" + } else { + creds.User = "admin" + } + } + + // Password + if p := os.Getenv(prefix1 + "_PASSWORD"); p != "" { + creds.Password = p + } else if p := os.Getenv(prefix2 + "_PASSWORD"); p != "" { + creds.Password = p + } else if p := os.Getenv("APPLIANCE_PASSWORD"); p != "" { + creds.Password = p + } + + // Cert + if flagCert != "" { + creds.Cert = flagCert + } else if c := os.Getenv(prefix1 + "_CERT"); c != "" { + creds.Cert = c + } else if c := os.Getenv(prefix2 + "_CERT"); c != "" { + creds.Cert = c + } + + // Cert Key + if flagKey != "" { + creds.Key = flagKey + } else if k := os.Getenv(prefix1 + "_KEY"); k != "" { + creds.Key = k + } else if k := os.Getenv(prefix2 + "_KEY"); k != "" { + creds.Key = k + } + + // SSH Identity + if flagIdentity != "" { + creds.Identity = flagIdentity + } else if i := os.Getenv(prefix1 + "_SSH_KEY"); i != "" { + creds.Identity = i + } else if i := os.Getenv(prefix2 + "_SSH_KEY"); i != "" { + creds.Identity = i + } else if i := os.Getenv("SSH_KEY_PATH"); i != "" { + creds.Identity = i + } + + return creds +} + // Appliance & Push Commands (BIG-IP, BIG-IQ) // ---------------------------------------------------------------------------- @@ -1872,7 +2003,7 @@ func handlePush(ctx context.Context, args []string) { method := "auto" destPath := "" keyPath := "" - targetKind := "BIG-IP" + targetKind := "auto" certPath := "" certKeyPath := "" applyGeoIP := false @@ -1925,6 +2056,8 @@ func handlePush(ctx context.Context, args []string) { targ := args[i+1] if strings.EqualFold(targ, "bigiq") || strings.EqualFold(targ, "big-iq") { targetKind = "BIG-IQ" + } else { + targetKind = "BIG-IP" } i++ } @@ -1932,6 +2065,8 @@ func handlePush(ctx context.Context, args []string) { targ := strings.TrimPrefix(arg, "--target=") if strings.EqualFold(targ, "bigiq") || strings.EqualFold(targ, "big-iq") { targetKind = "BIG-IQ" + } else { + targetKind = "BIG-IP" } } else if arg == "--cert" { if i+1 < len(args) { @@ -1960,23 +2095,6 @@ func handlePush(ctx context.Context, args []string) { } } - if certPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certPath = os.Getenv("BIGIQ_CERT") - } - if certPath == "" { - certPath = os.Getenv("BIGIP_CERT") - } - } - if certKeyPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certKeyPath = os.Getenv("BIGIQ_KEY") - } - if certKeyPath == "" { - certKeyPath = os.Getenv("BIGIP_KEY") - } - } - if len(positional) < 2 { printPushUsage() exitFunc(1) @@ -1984,7 +2102,10 @@ func handlePush(ctx context.Context, args []string) { host := positional[0] localFile := positional[1] - executePushToAppliance(ctx, host, localFile, method, destPath, user, port, keyPath, targetKind, certPath, certKeyPath, applyGeoIP, keepExtracted, jsonOutput) + + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp" || method == "ssh") + + executePushToAppliance(ctx, host, localFile, method, destPath, creds.User, port, creds.Identity, creds.TargetKind, creds.Cert, creds.Key, applyGeoIP, keepExtracted, jsonOutput) } func handlePull(ctx context.Context, args []string) { @@ -1999,7 +2120,7 @@ func handlePull(ctx context.Context, args []string) { method := "auto" localDest := "" keyPath := "" - targetKind := "BIG-IP" + targetKind := "auto" certPath := "" certKeyPath := "" @@ -2051,6 +2172,8 @@ func handlePull(ctx context.Context, args []string) { targ := args[i+1] if strings.EqualFold(targ, "bigiq") || strings.EqualFold(targ, "big-iq") { targetKind = "BIG-IQ" + } else { + targetKind = "BIG-IP" } i++ } @@ -2058,6 +2181,8 @@ func handlePull(ctx context.Context, args []string) { targ := strings.TrimPrefix(arg, "--target=") if strings.EqualFold(targ, "bigiq") || strings.EqualFold(targ, "big-iq") { targetKind = "BIG-IQ" + } else { + targetKind = "BIG-IP" } } else if arg == "--cert" { if i+1 < len(args) { @@ -2080,23 +2205,6 @@ func handlePull(ctx context.Context, args []string) { } } - if certPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certPath = os.Getenv("BIGIQ_CERT") - } - if certPath == "" { - certPath = os.Getenv("BIGIP_CERT") - } - } - if certKeyPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certKeyPath = os.Getenv("BIGIQ_KEY") - } - if certKeyPath == "" { - certKeyPath = os.Getenv("BIGIP_KEY") - } - } - if len(positional) < 2 { printPullUsage() exitFunc(1) @@ -2104,13 +2212,54 @@ func handlePull(ctx context.Context, args []string) { host := positional[0] remoteFile := positional[1] - executeDownloadFromAppliance(ctx, host, remoteFile, localDest, method, user, port, keyPath, targetKind, certPath, certKeyPath) + + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp" || method == "ssh") + + executeDownloadFromAppliance(ctx, host, remoteFile, localDest, method, creds.User, port, creds.Identity, creds.TargetKind, creds.Cert, creds.Key) +} + +func detectApplianceKind(ctx context.Context, host string, port int, user, pw string, pwBytes []byte, certPath, certKeyPath, fallbackKind string) string { + if pw != "" || len(pwBytes) > 0 || certPath != "" { + detectCtx, cancel := context.WithTimeout(ctx, 4*time.Second) + defer cancel() + c, err := bigip.NewClientWithOptions(bigip.ClientOptions{ + Host: host, + Port: port, + Username: user, + Password: pw, + SkipTLSVerify: true, + ClientCertPath: certPath, + ClientKeyPath: certKeyPath, + Timeout: 4 * time.Second, + }) + if err == nil { + defer c.Zero() + if kind, kErr := c.CheckReadyWithKind(detectCtx); kErr == nil && kind != "" { + return kind + } + } + } + if strings.EqualFold(fallbackKind, "BIG-IQ") { + return "BIG-IQ" + } + if strings.EqualFold(fallbackKind, "BIG-IP") { + return "BIG-IP" + } + return "Appliance" } func handleAppliance(ctx context.Context, targetKind string, args []string) { - cmdName := "bigip" - if strings.EqualFold(targetKind, "BIG-IQ") { + cmdName := strings.ToLower(targetKind) + switch cmdName { + case "bigiq", "big-iq": + targetKind = "BIG-IQ" cmdName = "bigiq" + case "bigip", "big-ip": + targetKind = "BIG-IP" + cmdName = "bigip" + default: + targetKind = "auto" + cmdName = "appliance" } if len(args) == 0 { @@ -2206,23 +2355,6 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { } } - if certPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certPath = os.Getenv("BIGIQ_CERT") - } - if certPath == "" { - certPath = os.Getenv("BIGIP_CERT") - } - } - if certKeyPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certKeyPath = os.Getenv("BIGIQ_KEY") - } - if certKeyPath == "" { - certKeyPath = os.Getenv("BIGIP_KEY") - } - } - method = strings.ToLower(strings.TrimSpace(method)) if method != "auto" && method != "icontrol" && method != "scp" && method != "ssh" { fmt.Fprintf(os.Stderr, "[ERROR] Invalid method %q. Valid methods: 'auto', 'icontrol', 'scp'\n", method) @@ -2239,27 +2371,13 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { exitFunc(1) } host := positional[0] - if user == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - user = os.Getenv("BIGIQ_USER") - } - if user == "" { - user = os.Getenv("BIGIP_USER") - } - if user == "" { - user = "admin" - } - } + + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + targetKind = creds.TargetKind - pw := "" - if strings.EqualFold(targetKind, "BIG-IQ") { - pw = os.Getenv("BIGIQ_PASSWORD") - } - if pw == "" { - pw = os.Getenv("BIGIP_PASSWORD") - } - if pw == "" && certPath == "" && term.IsTerminal(int(os.Stdin.Fd())) { - fmt.Printf("Enter %s password for %s@%s: ", targetKind, user, host) + pw := creds.Password + if pw == "" && creds.Cert == "" && term.IsTerminal(int(os.Stdin.Fd())) { + fmt.Printf("Enter %s password for %s@%s: ", targetKind, creds.User, host) bytePw, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err == nil { @@ -2268,7 +2386,7 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { auth.ZeroBytes(bytePw) } } - if pw == "" && certPath == "" { + if pw == "" && creds.Cert == "" { fmt.Fprintf(os.Stderr, "[ERROR] Password or client certificate required. Set BIGIP_PASSWORD / BIGIQ_PASSWORD / BIGIP_CERT or enter at prompt.\n") exitFunc(1) } @@ -2280,21 +2398,33 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { client, err := bigip.NewClientWithOptions(bigip.ClientOptions{ Host: host, Port: port, - Username: user, + Username: creds.User, Password: pw, PasswordBytes: pwBytes, SkipTLSVerify: true, - ClientCertPath: certPath, - ClientKeyPath: certKeyPath, + ClientCertPath: creds.Cert, + ClientKeyPath: creds.Key, }) checkErr(ctx, err, "Failed to initialize appliance client") defer client.Zero() - ready, err := client.CheckReady(ctx) + + var kind string + if client != nil { + kind, err = client.CheckReadyWithKind(ctx) + } else { + // CheckReady fallback? + kind = targetKind + } + checkErr(ctx, err, "Ready check failed") - if ready { + if kind != "" { + targetKind = kind fmt.Printf("%s appliance at %s is READY (HTTP 200).\n", targetKind, host) } else { - fmt.Printf("%s appliance at %s is NOT READY.\n", targetKind, host) + if targetKind == "auto" { + targetKind = "Appliance" + } + fmt.Printf("%s at %s is NOT READY.\n", targetKind, host) } case "info", "system-info", "sysinfo": @@ -2303,7 +2433,8 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { exitFunc(1) } target := positional[0] - info := getSystemInfoTarget(ctx, target, targetKind, user, port, keyPath, method, certPath, certKeyPath) + creds := resolveApplianceCreds(target, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + info := getSystemInfoTarget(ctx, target, creds.TargetKind, creds.User, port, creds.Identity, method, creds.Cert, creds.Key) if jsonOutput { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") @@ -2320,7 +2451,8 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { exitFunc(1) } target := positional[0] - info := getSystemInfoTarget(ctx, target, targetKind, user, port, keyPath, method, certPath, certKeyPath) + creds := resolveApplianceCreds(target, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + info := getSystemInfoTarget(ctx, target, creds.TargetKind, creds.User, port, creds.Identity, method, creds.Cert, creds.Key) sn := info.PreferredSerialNumber() if jsonOutput { res := map[string]string{ @@ -2349,13 +2481,10 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { } host := positional[0] localFile := positional[1] - executePushToAppliance(ctx, host, localFile, method, destPath, user, port, keyPath, targetKind, certPath, certKeyPath, applyGeoIP, keepExtracted, jsonOutput) + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + executePushToAppliance(ctx, host, localFile, method, destPath, creds.User, port, creds.Identity, creds.TargetKind, creds.Cert, creds.Key, applyGeoIP, keepExtracted, jsonOutput) case "geoip": - if strings.EqualFold(targetKind, "BIG-IQ") { - fmt.Fprintf(os.Stderr, "[ERROR] GeoIP database updates are only supported on BIG-IP appliances.\n") - exitFunc(1) - } if len(positional) < 2 { fmt.Fprintf(os.Stderr, "Usage: myf5 %s geoip [--method auto|icontrol|scp] [--dest ] [--keep-extracted] [--user ] [--port ] [-i ] [--cert ] [--cert-key ] [--json]\n", cmdName) exitFunc(1) @@ -2363,9 +2492,26 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { host := positional[0] zipTarget := positional[1] + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + + if creds.TargetKind == "auto" { + // Note: ApplyGeoIP will error out if it detects BIG-IQ, but we can do a quick check + // We can leave TargetKind as is; we will pass it. + // Let's pass "BIG-IP" if it's auto to bypass initial blocks in legacy logic, + // ApplyGeoIP will handle actual check. But ApplyGeoIP in geoip.go takes TargetType. + // We will just let ApplyGeoIP do its thing, or detect it. + if targetKind == "BIG-IQ" { + fmt.Fprintf(os.Stderr, "[ERROR] GeoIP database updates are only supported on BIG-IP appliances.\n") + exitFunc(1) + } + } else if strings.EqualFold(creds.TargetKind, "BIG-IQ") { + fmt.Fprintf(os.Stderr, "[ERROR] GeoIP database updates are only supported on BIG-IP appliances.\n") + exitFunc(1) + } + if fi, err := os.Stat(zipTarget); err == nil && !fi.IsDir() { // Local file: push it to appliance and apply - executePushToAppliance(ctx, host, zipTarget, method, destPath, user, port, keyPath, targetKind, certPath, certKeyPath, true, keepExtracted, jsonOutput) + executePushToAppliance(ctx, host, zipTarget, method, destPath, creds.User, port, creds.Identity, creds.TargetKind, creds.Cert, creds.Key, true, keepExtracted, jsonOutput) } else { // Remote file on appliance: apply directly if err := bigip.ValidateRemoteAppliancePath(zipTarget); err != nil { @@ -2373,24 +2519,10 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { exitFunc(1) } - if user == "" { - user = os.Getenv("BIGIP_USER") - if user == "" { - user = "admin" - } - } - pw := os.Getenv("BIGIP_PASSWORD") - if keyPath == "" { - if k := os.Getenv("BIGIP_SSH_KEY"); k != "" { - keyPath = k - } else if k := os.Getenv("SSH_KEY_PATH"); k != "" { - keyPath = k - } - } - - if pw == "" && keyPath == "" && certPath == "" { + pw := creds.Password + if pw == "" && creds.Identity == "" && creds.Cert == "" { if !hasDefaultSSHAuth() && term.IsTerminal(int(os.Stdin.Fd())) { - fmt.Printf("Enter %s password for %s@%s: ", targetKind, user, host) + fmt.Printf("Enter %s password for %s@%s: ", creds.TargetKind, creds.User, host) bytePw, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err == nil { @@ -2412,14 +2544,15 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { } geoOpts := bigip.GeoIPOptions{ Port: port, - Username: user, + Username: creds.User, Password: pw, PasswordBytes: pwBytes, - IdentityFile: keyPath, - ClientCertPath: certPath, - ClientKeyPath: certKeyPath, + IdentityFile: creds.Identity, + ClientCertPath: creds.Cert, + ClientKeyPath: creds.Key, Method: bigip.TransferMethod(method), KeepExtracted: keepExtracted, + TargetType: creds.TargetKind, } defer geoOpts.Zero() @@ -2468,7 +2601,8 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { } host := positional[0] remoteFile := positional[1] - executeDownloadFromAppliance(ctx, host, remoteFile, destPath, method, user, port, keyPath, targetKind, certPath, certKeyPath) + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + executeDownloadFromAppliance(ctx, host, remoteFile, destPath, method, creds.User, port, creds.Identity, creds.TargetKind, creds.Cert, creds.Key) case "qkview": if len(positional) < 1 { @@ -2476,24 +2610,15 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { exitFunc(1) } host := positional[0] - if user == "" { - user = os.Getenv("BIGIP_USER") - if user == "" { - user = "admin" - } - } - pw := os.Getenv("BIGIP_PASSWORD") - if keyPath == "" { - if k := os.Getenv("BIGIP_SSH_KEY"); k != "" { - keyPath = k - } else if k := os.Getenv("SSH_KEY_PATH"); k != "" { - keyPath = k - } - } + + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp" || method == "ssh") + targetKind = creds.TargetKind - if pw == "" && keyPath == "" && certPath == "" { + pw := creds.Password + + if pw == "" && creds.Identity == "" && creds.Cert == "" { if !hasDefaultSSHAuth() && term.IsTerminal(int(os.Stdin.Fd())) { - fmt.Printf("Enter %s password for %s@%s: ", targetKind, user, host) + fmt.Printf("Enter %s password for %s@%s: ", targetKind, creds.User, host) bytePw, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err == nil { @@ -2529,13 +2654,13 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { qkOpts := bigip.TransferOptions{ TargetType: targetKind, Method: transferMethod, - Username: user, + Username: creds.User, Password: pw, PasswordBytes: pwBytes, Port: port, - IdentityFile: keyPath, - ClientCertPath: certPath, - ClientKeyPath: certKeyPath, + IdentityFile: creds.Identity, + ClientCertPath: creds.Cert, + ClientKeyPath: creds.Key, Timeout: 30 * time.Second, } defer qkOpts.Zero() @@ -2639,45 +2764,21 @@ func getSystemInfoTarget(ctx context.Context, target, targetKind, user string, p } host := target - if user == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - user = os.Getenv("BIGIQ_USER") - } - if user == "" { - user = os.Getenv("BIGIP_USER") - } - if user == "" { - user = "admin" - } - } - - if certPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certPath = os.Getenv("BIGIQ_CERT") - } - if certPath == "" { - certPath = os.Getenv("BIGIP_CERT") - } - } - if certKeyPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certKeyPath = os.Getenv("BIGIQ_KEY") - } - if certKeyPath == "" { - certKeyPath = os.Getenv("BIGIP_KEY") - } - } - - pw := "" - if strings.EqualFold(targetKind, "BIG-IQ") { - pw = os.Getenv("BIGIQ_PASSWORD") - } - if pw == "" { - pw = os.Getenv("BIGIP_PASSWORD") - } + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp" || method == "ssh") + user = creds.User + certPath = creds.Cert + certKeyPath = creds.Key + keyPath = creds.Identity + targetKind = creds.TargetKind + + pw := creds.Password if pw == "" && keyPath == "" && certPath == "" { if !hasDefaultSSHAuth() && term.IsTerminal(int(os.Stdin.Fd())) { - fmt.Printf("Enter %s password for %s@%s: ", targetKind, user, host) + promptKind := targetKind + if promptKind == "" || promptKind == "auto" { + promptKind = "appliance" + } + fmt.Printf("Enter %s password for %s@%s: ", promptKind, user, host) bytePw, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err == nil { @@ -2807,58 +2908,21 @@ func executePushToAppliance( fileSize := stat.Size() baseName := filepath.Base(localPath) - if user == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - user = os.Getenv("BIGIQ_USER") - } - if user == "" { - user = os.Getenv("BIGIP_USER") - } - if user == "" { - if method == "scp" { - user = "root" - } else { - user = "admin" - } - } - } - - if certPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certPath = os.Getenv("BIGIQ_CERT") - } - if certPath == "" { - certPath = os.Getenv("BIGIP_CERT") - } - } - if certKeyPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certKeyPath = os.Getenv("BIGIQ_KEY") - } - if certKeyPath == "" { - certKeyPath = os.Getenv("BIGIP_KEY") - } - } - - pw := "" - if strings.EqualFold(targetKind, "BIG-IQ") { - pw = os.Getenv("BIGIQ_PASSWORD") - } - if pw == "" { - pw = os.Getenv("BIGIP_PASSWORD") - } - - if keyPath == "" { - if k := os.Getenv("BIGIP_SSH_KEY"); k != "" { - keyPath = k - } else if k := os.Getenv("SSH_KEY_PATH"); k != "" { - keyPath = k - } - } + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + user = creds.User + certPath = creds.Cert + certKeyPath = creds.Key + keyPath = creds.Identity + targetKind = creds.TargetKind + pw := creds.Password if pw == "" && keyPath == "" && certPath == "" { if !hasDefaultSSHAuth() && term.IsTerminal(int(os.Stdin.Fd())) { - fmt.Printf("Enter %s password for %s@%s: ", targetKind, user, host) + promptKind := targetKind + if promptKind == "" || promptKind == "auto" { + promptKind = "appliance" + } + fmt.Printf("Enter %s password for %s@%s: ", promptKind, user, host) bytePw, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err == nil { @@ -2874,6 +2938,12 @@ func executePushToAppliance( pwBytes = []byte(pw) } + targetKind = detectApplianceKind(ctx, host, port, user, pw, pwBytes, certPath, certKeyPath, targetKind) + if applyGeoIP && strings.EqualFold(targetKind, "BIG-IQ") { + fmt.Fprintf(os.Stderr, "[ERROR] Autodetected BIG-IQ appliance on %s: GeoIP database updates are only supported on BIG-IP appliances.\n", host) + exitFunc(1) + } + opts := bigip.TransferOptions{ TargetType: targetKind, Method: bigip.TransferMethod(method), @@ -3060,58 +3130,21 @@ func executeDownloadFromAppliance( localDest = filepath.Join(getDefaultDownloadsDir(), base) } - if user == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - user = os.Getenv("BIGIQ_USER") - } - if user == "" { - user = os.Getenv("BIGIP_USER") - } - if user == "" { - if method == "scp" { - user = "root" - } else { - user = "admin" - } - } - } - - if certPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certPath = os.Getenv("BIGIQ_CERT") - } - if certPath == "" { - certPath = os.Getenv("BIGIP_CERT") - } - } - if certKeyPath == "" { - if strings.EqualFold(targetKind, "BIG-IQ") { - certKeyPath = os.Getenv("BIGIQ_KEY") - } - if certKeyPath == "" { - certKeyPath = os.Getenv("BIGIP_KEY") - } - } - - pw := "" - if strings.EqualFold(targetKind, "BIG-IQ") { - pw = os.Getenv("BIGIQ_PASSWORD") - } - if pw == "" { - pw = os.Getenv("BIGIP_PASSWORD") - } - - if keyPath == "" { - if k := os.Getenv("BIGIP_SSH_KEY"); k != "" { - keyPath = k - } else if k := os.Getenv("SSH_KEY_PATH"); k != "" { - keyPath = k - } - } + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") + user = creds.User + certPath = creds.Cert + certKeyPath = creds.Key + keyPath = creds.Identity + targetKind = creds.TargetKind + pw := creds.Password if pw == "" && keyPath == "" && certPath == "" { if !hasDefaultSSHAuth() && term.IsTerminal(int(os.Stdin.Fd())) { - fmt.Printf("Enter %s password for %s@%s: ", targetKind, user, host) + promptKind := targetKind + if promptKind == "" || promptKind == "auto" { + promptKind = "appliance" + } + fmt.Printf("Enter %s password for %s@%s: ", promptKind, user, host) bytePw, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err == nil { @@ -3127,6 +3160,8 @@ func executeDownloadFromAppliance( pwBytes = []byte(pw) } + targetKind = detectApplianceKind(ctx, host, port, user, pw, pwBytes, certPath, certKeyPath, targetKind) + opts := bigip.TransferOptions{ TargetType: targetKind, Method: bigip.TransferMethod(method), diff --git a/pkg/bigip/client.go b/pkg/bigip/client.go index ddb5afa..cb9a324 100644 --- a/pkg/bigip/client.go +++ b/pkg/bigip/client.go @@ -336,23 +336,23 @@ func (c *Client) BaseURL() string { return fmt.Sprintf("%s://%s:%d", scheme, host, c.Port) } -// CheckReady queries /mgmt/tm/sys/ready (or /mgmt/shared/echo for BIG-IQ) to verify appliance availability. -func (c *Client) CheckReady(ctx context.Context) (bool, error) { +// CheckReadyWithKind queries /mgmt/tm/sys/ready (or /mgmt/shared/echo for BIG-IQ) to verify appliance availability and returns the appliance kind. +func (c *Client) CheckReadyWithKind(ctx context.Context) (string, error) { url := fmt.Sprintf("%s/mgmt/tm/sys/ready", c.BaseURL()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return false, err + return "", err } c.setAuth(req) resp, err := c.HTTPClient.Do(req) if err != nil { - return false, fmt.Errorf("device reachability check failed: %w", err) + return "", fmt.Errorf("device reachability check failed: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { - return true, nil + return "BIG-IP", nil } // Fallback to /mgmt/shared/echo for BIG-IQ and appliances without sys ready @@ -363,12 +363,23 @@ func (c *Client) CheckReady(ctx context.Context) (bool, error) { c.setAuth(echoReq) if echoResp, doErr := c.HTTPClient.Do(echoReq); doErr == nil { defer echoResp.Body.Close() - return echoResp.StatusCode == http.StatusOK, nil + if echoResp.StatusCode == http.StatusOK { + return "BIG-IQ", nil + } } } } - return false, nil + return "", nil +} + +// CheckReady queries /mgmt/tm/sys/ready (or /mgmt/shared/echo for BIG-IQ) to verify appliance availability. +func (c *Client) CheckReady(ctx context.Context) (bool, error) { + kind, err := c.CheckReadyWithKind(ctx) + if err != nil { + return false, err + } + return kind != "", nil } var validQKViewFilenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-\.]+$`) @@ -609,6 +620,33 @@ func (c *Client) GenerateQKViewWithStatus(ctx context.Context, filename string, } defer res.resp.Body.Close() + if res.resp.StatusCode == http.StatusNotFound { + // Fallback to bash for BIG-IQ where /mgmt/tm/util/qkview may return 404 + bashArgs := fmt.Sprintf("-c \"mkdir -p /var/tmp/qkviews && /usr/bin/qkview %s-f /var/tmp/qkviews/%s && cp /var/tmp/qkviews/%s /var/config/rest/downloads/\"", map[bool]string{true: "-s0 ", false: ""}[complete], filename, filename) + bashPayload := map[string]string{ + "command": "run", + "utilCmdArgs": bashArgs, + } + bashBytes, _ := json.Marshal(bashPayload) + bashURL := fmt.Sprintf("%s/mgmt/tm/util/bash", c.BaseURL()) + bashReq, err := http.NewRequestWithContext(ctx, http.MethodPost, bashURL, bytes.NewReader(bashBytes)) + if err != nil { + return "", err + } + c.setAuth(bashReq) + bashReq.Header.Set("Content-Type", "application/json") + bashResp, err := c.HTTPClient.Do(bashReq) + if err != nil { + return "", fmt.Errorf("bash fallback for qkview failed: %w", err) + } + defer bashResp.Body.Close() + if bashResp.StatusCode < 200 || bashResp.StatusCode >= 300 { + b, _ := io.ReadAll(io.LimitReader(bashResp.Body, 1024*1024)) + return "", fmt.Errorf("bash qkview generation failed with HTTP %d: %s", bashResp.StatusCode, string(b)) + } + return filename, nil + } + if res.resp.StatusCode < 200 || res.resp.StatusCode >= 300 { b, _ := io.ReadAll(io.LimitReader(res.resp.Body, 1024*1024)) return "", fmt.Errorf("qkview generation failed with HTTP %d: %s", res.resp.StatusCode, string(b)) diff --git a/pkg/bigip/client_test.go b/pkg/bigip/client_test.go index f3504ef..43741fc 100644 --- a/pkg/bigip/client_test.go +++ b/pkg/bigip/client_test.go @@ -16,11 +16,12 @@ import ( func TestCheckReady(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/mgmt/tm/sys/ready" { - t.Errorf("unexpected path: %s", r.URL.Path) + if r.URL.Path == "/mgmt/tm/sys/ready" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"kind": "tm:sys:ready"}`)) + return } - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"kind": "tm:sys:ready"}`)) + t.Errorf("unexpected path: %s", r.URL.Path) })) defer server.Close() @@ -38,6 +39,44 @@ func TestCheckReady(t *testing.T) { if !ready { t.Errorf("expected appliance to be ready") } + + kind, err := c.CheckReadyWithKind(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if kind != "BIG-IP" { + t.Errorf("expected BIG-IP, got %q", kind) + } +} + +func TestCheckReadyWithKind_BIGIQ(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/mgmt/tm/sys/ready" { + w.WriteHeader(http.StatusNotFound) + return + } + if r.URL.Path == "/mgmt/shared/echo" { + w.WriteHeader(http.StatusOK) + return + } + t.Errorf("unexpected path: %s", r.URL.Path) + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + port, _ := strconv.Atoi(u.Port()) + + c := NewClient(u.Hostname(), port, "admin", "secret") + c.Scheme = "http" + c.HTTPClient = server.Client() + + kind, err := c.CheckReadyWithKind(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if kind != "BIG-IQ" { + t.Errorf("expected BIG-IQ, got %q", kind) + } } func TestBaseURLAndURLNormalization(t *testing.T) { @@ -195,6 +234,47 @@ func TestGenerateQKViewDefaultFilenameISO8601(t *testing.T) { } } +func TestGenerateQKViewBIGIQFallback(t *testing.T) { + bashCalled := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.URL.Path == "/mgmt/tm/util/qkview" { + w.WriteHeader(http.StatusNotFound) + return + } + if r.URL.Path == "/mgmt/tm/util/bash" { + bashCalled = true + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"commandResult":"success"}`)) + return + } + if r.URL.Path == "/mgmt/cm/autodeploy/qkview" { + w.WriteHeader(http.StatusNotFound) + return + } + })) + defer server.Close() + + u, _ := url.Parse(server.URL) + port, _ := strconv.Atoi(u.Port()) + + c := NewClient(u.Hostname(), port, "admin", "secret") + c.Scheme = "http" + c.HTTPClient = server.Client() + + filename, err := c.GenerateQKViewWithStatus(context.Background(), "test.qkview", true, nil) + if err != nil { + t.Fatalf("GenerateQKViewWithStatus failed: %v", err) + } + + if filename != "test.qkview" { + t.Errorf("expected test.qkview, got %q", filename) + } + if !bashCalled { + t.Errorf("expected bash fallback to be called for BIG-IQ") + } +} + func TestValidateHost(t *testing.T) { tests := []struct { name string diff --git a/pkg/bigip/failover.go b/pkg/bigip/failover.go index 0a2f751..cfc65f3 100644 --- a/pkg/bigip/failover.go +++ b/pkg/bigip/failover.go @@ -183,16 +183,10 @@ func DownloadFileWithFailover( return savedPath, MethodSCP, nil case MethodAuto: - // Try iControl REST first if password is provided or file looks like an autodeploy resource (/shared/images or /var/tmp/qkviews) - cleanRemote := path.Clean(remotePath) - isRestFriendly := strings.HasPrefix(cleanRemote, "/shared/images") || - strings.HasPrefix(cleanRemote, "/var/tmp/qkviews") || - strings.HasPrefix(cleanRemote, "/mgmt/") || - strings.HasSuffix(cleanRemote, ".iso") || - strings.HasSuffix(cleanRemote, ".qkview") - + // If an explicit SSH identity key (-i) was provided, prefer SCP first. + // Otherwise, try iControl REST first when REST credentials (password/cert) are available. hasRestAuth := opts.Password != "" || len(opts.PasswordBytes) > 0 || opts.ClientCertPath != "" || os.Getenv("BIGIP_PASSWORD") != "" || os.Getenv("BIGIQ_PASSWORD") != "" - tryRestFirst := hasRestAuth && (isRestFriendly || opts.IdentityFile == "") + tryRestFirst := opts.IdentityFile == "" && hasRestAuth if tryRestFirst { savedPath, restErr := downloadViaIControl(ctx, host, remotePath, localDest, opts, progressCb) @@ -249,12 +243,17 @@ func newClientFromOptions(host string, opts TransferOptions, defaultPort int) *C if user == "" { user = "admin" } + var pwBytes []byte + if len(opts.PasswordBytes) > 0 { + pwBytes = make([]byte, len(opts.PasswordBytes)) + copy(pwBytes, opts.PasswordBytes) + } c, err := NewClientWithOptions(ClientOptions{ Host: host, Port: port, Username: user, Password: opts.Password, - PasswordBytes: opts.PasswordBytes, + PasswordBytes: pwBytes, SkipTLSVerify: true, ClientCertPath: opts.ClientCertPath, ClientKeyPath: opts.ClientKeyPath, @@ -296,23 +295,37 @@ func pushViaIControl( return remoteUploaded, nil } -func pushViaSCP( - ctx context.Context, - host, localFile, destDir string, - opts TransferOptions, - progressCb func(transferred, total int64), -) (string, error) { - scpOpts := SCPOptions{ +func newSCPOptions(host string, opts TransferOptions) SCPOptions { + var pwBytes []byte + if len(opts.PasswordBytes) > 0 { + pwBytes = make([]byte, len(opts.PasswordBytes)) + copy(pwBytes, opts.PasswordBytes) + } + var passBytes []byte + if len(opts.SSHPassphraseBytes) > 0 { + passBytes = make([]byte, len(opts.SSHPassphraseBytes)) + copy(passBytes, opts.SSHPassphraseBytes) + } + return SCPOptions{ Host: host, Port: opts.Port, Username: opts.Username, Password: opts.Password, - PasswordBytes: opts.PasswordBytes, + PasswordBytes: pwBytes, KeyPath: opts.IdentityFile, Passphrase: opts.SSHPassphrase, - PassphraseBytes: opts.SSHPassphraseBytes, + PassphraseBytes: passBytes, Timeout: opts.Timeout, } +} + +func pushViaSCP( + ctx context.Context, + host, localFile, destDir string, + opts TransferOptions, + progressCb func(transferred, total int64), +) (string, error) { + scpOpts := newSCPOptions(host, opts) defer scpOpts.Zero() return SCPUpload(ctx, localFile, destDir, scpOpts, progressCb) } @@ -348,17 +361,7 @@ func downloadViaSCP( opts TransferOptions, progressCb func(transferred, total int64), ) (string, error) { - scpOpts := SCPOptions{ - Host: host, - Port: opts.Port, - Username: opts.Username, - Password: opts.Password, - PasswordBytes: opts.PasswordBytes, - KeyPath: opts.IdentityFile, - Passphrase: opts.SSHPassphrase, - PassphraseBytes: opts.SSHPassphraseBytes, - Timeout: opts.Timeout, - } + scpOpts := newSCPOptions(host, opts) defer scpOpts.Zero() return SCPDownload(ctx, remotePath, localDest, scpOpts, progressCb) } @@ -390,17 +393,7 @@ func GenerateQKViewWithFailover( return filename, MethodIControl, nil case MethodSCP: - scpOpts := SCPOptions{ - Host: host, - Port: opts.Port, - Username: opts.Username, - Password: opts.Password, - PasswordBytes: opts.PasswordBytes, - KeyPath: opts.IdentityFile, - Passphrase: opts.SSHPassphrase, - PassphraseBytes: opts.SSHPassphraseBytes, - Timeout: opts.Timeout, - } + scpOpts := newSCPOptions(host, opts) defer scpOpts.Zero() filename, err := GenerateQKViewSSH(ctx, scpOpts, requestedFilename, complete, statusCb) if err != nil { @@ -410,7 +403,7 @@ func GenerateQKViewWithFailover( case MethodAuto: // If password or client certificate is provided and no key, try iControl REST first - tryRestFirst := opts.Password != "" || len(opts.PasswordBytes) > 0 || opts.ClientCertPath != "" || (opts.IdentityFile == "" && os.Getenv("BIGIP_PASSWORD") != "") + tryRestFirst := opts.IdentityFile == "" && (opts.Password != "" || len(opts.PasswordBytes) > 0 || opts.ClientCertPath != "" || os.Getenv("BIGIP_PASSWORD") != "") if tryRestFirst { client := newClientFromOptions(host, opts, 443) @@ -427,17 +420,7 @@ func GenerateQKViewWithFailover( logFn("iControl REST QKView generation failed (%v). Automatically falling back to SSH...", restErr) } - scpOpts := SCPOptions{ - Host: host, - Port: opts.Port, - Username: opts.Username, - Password: opts.Password, - PasswordBytes: opts.PasswordBytes, - KeyPath: opts.IdentityFile, - Passphrase: opts.SSHPassphrase, - PassphraseBytes: opts.SSHPassphraseBytes, - Timeout: opts.Timeout, - } + scpOpts := newSCPOptions(host, opts) defer scpOpts.Zero() filename, sshErr := GenerateQKViewSSH(ctx, scpOpts, requestedFilename, complete, statusCb) if sshErr == nil { @@ -448,17 +431,7 @@ func GenerateQKViewWithFailover( } // Try SSH first - scpOpts := SCPOptions{ - Host: host, - Port: opts.Port, - Username: opts.Username, - Password: opts.Password, - PasswordBytes: opts.PasswordBytes, - KeyPath: opts.IdentityFile, - Passphrase: opts.SSHPassphrase, - PassphraseBytes: opts.SSHPassphraseBytes, - Timeout: opts.Timeout, - } + scpOpts := newSCPOptions(host, opts) filename, sshErr := GenerateQKViewSSH(ctx, scpOpts, requestedFilename, complete, statusCb) scpOpts.Zero() if sshErr == nil { @@ -489,7 +462,7 @@ func GenerateQKViewWithFailover( // DeleteQKViewWithFailover deletes a generated QKView archive on the appliance via REST or SSH. func DeleteQKViewWithFailover(ctx context.Context, host, remoteFilename string, opts TransferOptions) error { method := opts.Method - if method == MethodIControl || (method == MethodAuto && (opts.Password != "" || len(opts.PasswordBytes) > 0 || opts.ClientCertPath != "")) { + if method == MethodIControl || (method == MethodAuto && opts.IdentityFile == "" && (opts.Password != "" || len(opts.PasswordBytes) > 0 || opts.ClientCertPath != "")) { client := newClientFromOptions(host, opts, 443) defer client.Zero() if err := client.DeleteQKView(ctx, remoteFilename); err == nil { @@ -499,17 +472,7 @@ func DeleteQKViewWithFailover(ctx context.Context, host, remoteFilename string, } } - scpOpts := SCPOptions{ - Host: host, - Port: opts.Port, - Username: opts.Username, - Password: opts.Password, - PasswordBytes: opts.PasswordBytes, - KeyPath: opts.IdentityFile, - Passphrase: opts.SSHPassphrase, - PassphraseBytes: opts.SSHPassphraseBytes, - Timeout: opts.Timeout, - } + scpOpts := newSCPOptions(host, opts) defer scpOpts.Zero() return DeleteQKViewSSH(ctx, scpOpts, remoteFilename) } diff --git a/pkg/bigip/failover_test.go b/pkg/bigip/failover_test.go index 8bca218..c611001 100644 --- a/pkg/bigip/failover_test.go +++ b/pkg/bigip/failover_test.go @@ -161,3 +161,32 @@ func TestDownloadFileWithFailover_BothFail(t *testing.T) { t.Errorf("expected failover logger to be invoked") } } + +func TestTransferOptionsPasswordBytesNotZeroedAcrossCalls(t *testing.T) { + opts := TransferOptions{ + PasswordBytes: []byte("secret123"), + SSHPassphraseBytes: []byte("pass123"), + } + + // 1. Verify client creation with newClientFromOptions does not zero opts.PasswordBytes + client := newClientFromOptions("10.0.0.1", opts, 443) + if client == nil { + t.Fatalf("expected non-nil client") + } + client.Zero() + + if string(opts.PasswordBytes) != "secret123" { + t.Errorf("expected opts.PasswordBytes to remain %q, got %q", "secret123", string(opts.PasswordBytes)) + } + + // 2. Verify newSCPOptions and scpOpts.Zero() do not zero opts.PasswordBytes or opts.SSHPassphraseBytes + scpOpts := newSCPOptions("10.0.0.1", opts) + scpOpts.Zero() + + if string(opts.PasswordBytes) != "secret123" { + t.Errorf("expected opts.PasswordBytes to remain %q after scpOpts.Zero(), got %q", "secret123", string(opts.PasswordBytes)) + } + if string(opts.SSHPassphraseBytes) != "pass123" { + t.Errorf("expected opts.SSHPassphraseBytes to remain %q after scpOpts.Zero(), got %q", "pass123", string(opts.SSHPassphraseBytes)) + } +} diff --git a/pkg/bigip/geoip.go b/pkg/bigip/geoip.go index aa78925..797c0a0 100644 --- a/pkg/bigip/geoip.go +++ b/pkg/bigip/geoip.go @@ -252,6 +252,12 @@ func generateGeoIPScript(remoteZipPath string, force, keepExtracted bool) string return fmt.Sprintf(`( set -o pipefail + +if grep -qi '^Product:[[:space:]]*BIG-IQ' /VERSION 2>/dev/null; then + echo "ERROR: Autodetected BIG-IQ appliance. GeoIP database updates are only supported on BIG-IP appliances." >&2 + exit 8 +fi + ZIP_FILE=%q if [ ! -f "$ZIP_FILE" ]; then @@ -333,15 +339,24 @@ exit $SCRIPT_EXIT_CODE } func applyGeoIPSSH(ctx context.Context, host, scriptB64 string, opts GeoIPOptions) (string, error) { + var pwCopy, ppCopy []byte + if len(opts.PasswordBytes) > 0 { + pwCopy = make([]byte, len(opts.PasswordBytes)) + copy(pwCopy, opts.PasswordBytes) + } + if len(opts.SSHPassphraseBytes) > 0 { + ppCopy = make([]byte, len(opts.SSHPassphraseBytes)) + copy(ppCopy, opts.SSHPassphraseBytes) + } scpOpts := SCPOptions{ Host: host, Port: opts.Port, Username: opts.Username, Password: opts.Password, - PasswordBytes: opts.PasswordBytes, + PasswordBytes: pwCopy, KeyPath: opts.IdentityFile, Passphrase: opts.SSHPassphrase, - PassphraseBytes: opts.SSHPassphraseBytes, + PassphraseBytes: ppCopy, Timeout: opts.Timeout, } defer scpOpts.Zero() @@ -422,12 +437,18 @@ func applyGeoIPREST(ctx context.Context, host, scriptB64 string, opts GeoIPOptio user = "admin" } + var pwCopy []byte + if len(opts.PasswordBytes) > 0 { + pwCopy = make([]byte, len(opts.PasswordBytes)) + copy(pwCopy, opts.PasswordBytes) + } + client, err := NewClientWithOptions(ClientOptions{ Host: host, Port: port, Username: user, Password: opts.Password, - PasswordBytes: opts.PasswordBytes, + PasswordBytes: pwCopy, SkipTLSVerify: true, ClientCertPath: opts.ClientCertPath, ClientKeyPath: opts.ClientKeyPath, diff --git a/pkg/bigip/info.go b/pkg/bigip/info.go index 9bea043..857f1b1 100644 --- a/pkg/bigip/info.go +++ b/pkg/bigip/info.go @@ -58,6 +58,14 @@ func (s *SystemInfo) PreferredSerialNumber() string { return s.RegistrationKey } +// ApplianceKind returns "BIG-IQ" if the appliance is detected as BIG-IQ, or "BIG-IP" otherwise. +func (s *SystemInfo) ApplianceKind() string { + if s.IsBIGIQ() { + return "BIG-IQ" + } + return "BIG-IP" +} + // IsBIGIQ returns true if the appliance is a BIG-IQ Centralized Management system. func (s *SystemInfo) IsBIGIQ() bool { if s == nil { diff --git a/pkg/bigip/info_test.go b/pkg/bigip/info_test.go index 41bbff8..5450724 100644 --- a/pkg/bigip/info_test.go +++ b/pkg/bigip/info_test.go @@ -530,6 +530,18 @@ Active License } } +func TestApplianceKind(t *testing.T) { + s1 := &SystemInfo{Product: "BIG-IQ"} + if got := s1.ApplianceKind(); got != "BIG-IQ" { + t.Errorf("ApplianceKind() = %v, want BIG-IQ", got) + } + + s2 := &SystemInfo{Product: "BIG-IP"} + if got := s2.ApplianceKind(); got != "BIG-IP" { + t.Errorf("ApplianceKind() = %v, want BIG-IP", got) + } +} + func TestSystemInfo_SummaryPlatformEnrichment(t *testing.T) { // Case 1: Raw platform code C119 info1 := &SystemInfo{Platform: "C119"} From 2c5ae70d04a83e8d40fc1475d9cf8e2a8e8fc8d6 Mon Sep 17 00:00:00 2001 From: Greg Robinson Date: Sat, 7 Nov 2026 01:20:56 -0500 Subject: [PATCH 2/2] style(cli): gofmt cmd/myf5/main.go --- cmd/myf5/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/myf5/main.go b/cmd/myf5/main.go index 87d9ae7..ddb9fc8 100644 --- a/cmd/myf5/main.go +++ b/cmd/myf5/main.go @@ -2371,7 +2371,7 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { exitFunc(1) } host := positional[0] - + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") targetKind = creds.TargetKind @@ -2407,7 +2407,7 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { }) checkErr(ctx, err, "Failed to initialize appliance client") defer client.Zero() - + var kind string if client != nil { kind, err = client.CheckReadyWithKind(ctx) @@ -2415,7 +2415,7 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { // CheckReady fallback? kind = targetKind } - + checkErr(ctx, err, "Ready check failed") if kind != "" { targetKind = kind @@ -2493,7 +2493,7 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { zipTarget := positional[1] creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp") - + if creds.TargetKind == "auto" { // Note: ApplyGeoIP will error out if it detects BIG-IQ, but we can do a quick check // We can leave TargetKind as is; we will pass it. @@ -2610,7 +2610,7 @@ func handleAppliance(ctx context.Context, targetKind string, args []string) { exitFunc(1) } host := positional[0] - + creds := resolveApplianceCreds(host, targetKind, user, certPath, certKeyPath, keyPath, method == "scp" || method == "ssh") targetKind = creds.TargetKind