From 7a7892b067a5b5e99fb52751b7383c028514e377 Mon Sep 17 00:00:00 2001 From: Pau Capdevila Date: Sat, 11 Jan 2025 22:18:00 +0100 Subject: [PATCH] tbs: refactor show-tech Signed-off-by: Pau Capdevila --- go.mod | 3 + go.sum | 8 + pkg/hhfab/show-tech/control.sh | 21 ++- pkg/hhfab/vlabhelpers.go | 328 +++++++++++++++++++-------------- 4 files changed, 216 insertions(+), 144 deletions(-) diff --git a/go.mod b/go.mod index d08c5b13..e1350195 100644 --- a/go.mod +++ b/go.mod @@ -64,11 +64,13 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.12.9 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/ScaleFT/sshkeys v1.2.0 // indirect github.com/Shopify/ejson v1.3.3 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/appleboy/easyssh-proxy v1.5.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect @@ -119,6 +121,7 @@ require ( github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect github.com/cyphar/filepath-securejoin v0.3.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect github.com/distribution/reference v0.6.0 // indirect github.com/djherbis/times v1.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect diff --git a/go.sum b/go.sum index 1b089aa4..c514837a 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEP github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ScaleFT/sshkeys v1.2.0 h1:5BRp6rTVIhJzXT3VcUQrKgXR8zWA3sOsNeuyW15WUA8= +github.com/ScaleFT/sshkeys v1.2.0/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o= github.com/Shopify/ejson v1.3.3 h1:dPzgmvFhUPTJIzwdF5DaqbwW1dWaoR8ADKRdSTy6Mss= github.com/Shopify/ejson v1.3.3/go.mod h1:VZMUtDzvBW/PAXRUF5fzp1ffb1ucT8MztrZXXLYZurw= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= @@ -79,6 +81,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/appleboy/easyssh-proxy v1.5.0 h1:OYdSPvYQN3mhnsMH5I2OF1TgwSEcSq33kvjQfTwvZww= +github.com/appleboy/easyssh-proxy v1.5.0/go.mod h1:zcEMrStH91/tcUn3gUGP0KpQwUYLm8tX/Ook1AH98uc= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= @@ -220,6 +224,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= @@ -919,6 +925,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1047,6 +1054,7 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/hhfab/show-tech/control.sh b/pkg/hhfab/show-tech/control.sh index 255bdfe0..b7f94494 100644 --- a/pkg/hhfab/show-tech/control.sh +++ b/pkg/hhfab/show-tech/control.sh @@ -10,6 +10,9 @@ OUTPUT_FILE="/tmp/show-tech.log" # Clear the log file : > "$OUTPUT_FILE" +# Set the kubectl path +KUBECTL="/opt/bin/kubectl" + echo "=== System Information ===" >> "$OUTPUT_FILE" uname -a >> "$OUTPUT_FILE" cat /etc/os-release >> "$OUTPUT_FILE" @@ -18,13 +21,13 @@ echo -e "\n=== K3s Version ===" >> "$OUTPUT_FILE" k3s --version >> "$OUTPUT_FILE" 2>/dev/null echo -e "\n=== Kubernetes Nodes ===" >> "$OUTPUT_FILE" -kubectl get nodes -o wide >> "$OUTPUT_FILE" 2>/dev/null +$KUBECTL get nodes -o wide >> "$OUTPUT_FILE" 2>/dev/null echo -e "\n=== Kubernetes Pods ===" >> "$OUTPUT_FILE" -kubectl get pods -A -o wide >> "$OUTPUT_FILE" 2>/dev/null +$KUBECTL get pods -A -o wide >> "$OUTPUT_FILE" 2>/dev/null echo -e "\n=== Kubernetes Events ===" >> "$OUTPUT_FILE" -kubectl get events -A >> "$OUTPUT_FILE" 2>/dev/null +$KUBECTL get events -A >> "$OUTPUT_FILE" 2>/dev/null echo -e "\n=== Disk Usage ===" >> "$OUTPUT_FILE" df -h >> "$OUTPUT_FILE" @@ -36,25 +39,25 @@ ps aux >> "$OUTPUT_FILE" echo -e "\n=== Githedgehog.com Resources ===" >> "$OUTPUT_FILE" -crds_githedgehog=$(kubectl get crds -o custom-columns=":metadata.name" | grep 'githedgehog.com') +crds_githedgehog=$($KUBECTL get crds -o custom-columns=":metadata.name" | grep 'githedgehog.com') for crd in $crds_githedgehog; do echo -e "\n=== Instances of $crd ===" >> "$OUTPUT_FILE" - kubectl get $crd -A >> "$OUTPUT_FILE" 2>/dev/null + $KUBECTL get $crd -A >> "$OUTPUT_FILE" 2>/dev/null done -resources_githedgehog=$(kubectl api-resources --verbs=list --namespaced=true -o name | grep 'githedgehog.com') +resources_githedgehog=$($KUBECTL api-resources --verbs=list --namespaced=true -o name | grep 'githedgehog.com') for resource in $resources_githedgehog; do echo -e "\n=== Instances of $resource ===" >> "$OUTPUT_FILE" - kubectl get $resource -A >> "$OUTPUT_FILE" 2>/dev/null + $KUBECTL get $resource -A >> "$OUTPUT_FILE" 2>/dev/null done -resources_non_namespaced_githedgehog=$(kubectl api-resources --verbs=list --namespaced=false -o name | grep 'githedgehog.com') +resources_non_namespaced_githedgehog=$($KUBECTL api-resources --verbs=list --namespaced=false -o name | grep 'githedgehog.com') for resource in $resources_non_namespaced_githedgehog; do echo -e "\n=== Instances of $resource (non-namespaced) ===" >> "$OUTPUT_FILE" - kubectl get $resource >> "$OUTPUT_FILE" 2>/dev/null + $KUBECTL get $resource >> "$OUTPUT_FILE" 2>/dev/null done echo "Diagnostics collected to $OUTPUT_FILE" diff --git a/pkg/hhfab/vlabhelpers.go b/pkg/hhfab/vlabhelpers.go index 21347d5b..d984a219 100644 --- a/pkg/hhfab/vlabhelpers.go +++ b/pkg/hhfab/vlabhelpers.go @@ -7,7 +7,7 @@ import ( "context" _ "embed" "fmt" - "io" + // "io" "log/slog" "net/netip" "os" @@ -18,13 +18,11 @@ import ( "sync" "time" + "github.com/appleboy/easyssh-proxy" "github.com/manifoldco/promptui" - "github.com/melbahja/goph" - "github.com/pkg/sftp" wiringapi "go.githedgehog.com/fabric/api/wiring/v1beta1" "go.githedgehog.com/fabric/pkg/hhfctl" "go.githedgehog.com/fabric/pkg/util/kubeutil" - "golang.org/x/crypto/ssh" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -53,33 +51,10 @@ func (c *Config) VLABAccess(ctx context.Context, vlab *VLAB, t VLABAccessType, n return err } - entries := map[string]VLABAccessInfo{} - for _, vm := range vlab.VMs { - sshPort := uint(0) - - if len(vm.NICs) > 0 && strings.Contains(vm.NICs[0], "user,") && (vm.Type == VMTypeControl || vm.Type == VMTypeServer) { - sshPort = getSSHPort(vm.ID) - } - - vmDir := filepath.Join(VLABDir, VLABVMsDir, vm.Name) - entries[vm.Name] = VLABAccessInfo{ - SSHPort: sshPort, - SerialSock: filepath.Join(vmDir, VLABSerialSock), - SerialLog: filepath.Join(vmDir, VLABSerialLog), - IsSwitch: vm.Type == VMTypeSwitch, - } - } - - switches := wiringapi.SwitchList{} - if err := c.Wiring.List(ctx, &switches); err != nil { - return fmt.Errorf("failed to list switches: %w", err) - } - - for _, sw := range switches.Items { - entry := entries[sw.Name] - entry.RemoteSerial = hhfctl.GetSerialInfo(&sw) - entry.IsSwitch = true - entries[sw.Name] = entry + // Get VM and Switch entries from VLAB + entries, err := c.getVLABEntries(ctx, vlab) + if err != nil { + return fmt.Errorf("retrieving VM and switch entries: %w", err) } if name == "" { @@ -240,10 +215,49 @@ type VLABAccessInfo struct { SSHPort uint // local ssh port SerialSock string SerialLog string - IsSwitch bool // ssh through control node only + IsSwitch bool // ssh through control node only + IsControl bool // Needed to distinguish VM type in show-tech + IsServer bool RemoteSerial string // ssh to get serial } +func (c *Config) getVLABEntries(ctx context.Context, vlab *VLAB) (map[string]VLABAccessInfo, error) { + entries := map[string]VLABAccessInfo{} + + // Gather VM entries + for _, vm := range vlab.VMs { + sshPort := uint(0) + + if len(vm.NICs) > 0 && strings.Contains(vm.NICs[0], "user,") && (vm.Type == VMTypeControl || vm.Type == VMTypeServer) { + sshPort = getSSHPort(vm.ID) + } + + vmDir := filepath.Join(VLABDir, VLABVMsDir, vm.Name) + entries[vm.Name] = VLABAccessInfo{ + SSHPort: sshPort, + SerialSock: filepath.Join(vmDir, VLABSerialSock), + SerialLog: filepath.Join(vmDir, VLABSerialLog), + IsSwitch: vm.Type == VMTypeSwitch, + IsServer: vm.Type == VMTypeServer, + IsControl: vm.Type == VMTypeControl, + } + } + + // Gather switch entries + switches := wiringapi.SwitchList{} + if err := c.Wiring.List(ctx, &switches); err != nil { + return nil, fmt.Errorf("failed to list switches: %w", err) + } + + for _, sw := range switches.Items { + entry := entries[sw.Name] + entry.RemoteSerial = hhfctl.GetSerialInfo(&sw) + entries[sw.Name] = entry + } + + return entries, nil +} + //go:embed show-tech/server.sh var serverScript []byte @@ -253,12 +267,14 @@ var controlScript []byte //go:embed show-tech/switch.sh var switchScript []byte -type ShowTechConfig struct { +// ShowTechScript represents scripts for different VM types +type ShowTechScript struct { Scripts map[VMType][]byte } -func DefaultShowTechConfig() ShowTechConfig { - return ShowTechConfig{ +// DefaultShowTechScript initializes scripts for VM types +func DefaultShowTechScript() ShowTechScript { + return ShowTechScript{ Scripts: map[VMType][]byte{ VMTypeServer: serverScript, VMTypeControl: controlScript, @@ -268,30 +284,44 @@ func DefaultShowTechConfig() ShowTechConfig { } func (c *Config) VLABShowTech(ctx context.Context, vlab *VLAB) error { - config := DefaultShowTechConfig() + // Get VM and Switch entries from VLAB + entries, err := c.getVLABEntries(ctx, vlab) + if err != nil { + return fmt.Errorf("retrieving VM and switch entries: %w", err) + } + // Initialize the ShowTechScript + scriptConfig := DefaultShowTechScript() + + // Create the output directory outputDir := filepath.Join(c.WorkDir, "show-tech-output") if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("creating output directory: %w", err) } + // Channel for errors and WaitGroup for concurrency var wg sync.WaitGroup - errChan := make(chan error, len(vlab.VMs)) + errChan := make(chan error, len(entries)) - for _, vm := range vlab.VMs { + // Iterate over entries + for name, entry := range entries { wg.Add(1) - go func(vm VM) { + go func(name string, entry VLABAccessInfo) { defer wg.Done() - slog.Debug("Collecting Show Tech bundle for", "vm", vm.Name) - if err := c.collectShowTech(ctx, vm, vlab, config, outputDir); err != nil { - errChan <- fmt.Errorf("collecting show-tech for VM %s: %w", vm.Name, err) + slog.Debug("Collecting Show Tech bundle for", "entry", name) + + // Pass entry, scriptConfig, and outputDir to collectShowTech + if err := c.collectShowTech(ctx, name, entry, scriptConfig, outputDir); err != nil { + errChan <- fmt.Errorf("collecting show-tech for entry %s: %w", name, err) } - }(vm) + }(name, entry) } + // Wait for all goroutines to finish wg.Wait() close(errChan) + // Collect and handle errors var errors []error for err := range errChan { errors = append(errors, err) @@ -305,142 +335,170 @@ func (c *Config) VLABShowTech(ctx context.Context, vlab *VLAB) error { return nil } -func (c *Config) collectShowTech(ctx context.Context, vm VM, vlab *VLAB, config ShowTechConfig, outputDir string) error { - script, ok := config.Scripts[vm.Type] +func (c *Config) collectShowTech(ctx context.Context, entryName string, entry VLABAccessInfo, scriptConfig ShowTechScript, outputDir string) error { + // Determine the script for the VM type + vmType := getVMType(entry) + script, ok := scriptConfig.Scripts[vmType] if !ok { - return nil // Skip VMs with no defined script + return nil // Skip entries with no defined script } - auth, err := goph.RawKey(vlab.SSHKey, "") - if err != nil { - return fmt.Errorf("getting ssh auth: %w", err) + // Create VM-specific output directory + vmOutputDir := filepath.Join(outputDir, entryName) + if err := os.MkdirAll(vmOutputDir, 0755); err != nil { + return fmt.Errorf("creating output directory for %s: %w", entryName, err) } - client, err := c.waitForSSH(ctx, vm, auth) + // Remote paths + remoteScriptPath := "/tmp/show-tech.sh" + remoteOutputPath := "/tmp/show-tech.log" + localOutputPath := filepath.Join(vmOutputDir, "show-tech.log") + + // Execute remote commands and file transfers using easyssh-proxy //nolint:goerr113 + ssh, err := c.createSSHConfig(ctx, entryName, entry) if err != nil { - return err + return fmt.Errorf("creating SSH config for %s: %w", entryName, err) } - defer client.Close() - sftpClient, err := sftp.NewClient(client.Client) + // Create temporary file for the script + tmpfile, err := os.CreateTemp("", "script-*") if err != nil { - return fmt.Errorf("creating SFTP client: %w", err) + return fmt.Errorf("creating temporary script file: %w", err) } - defer sftpClient.Close() + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() - vmOutputDir := filepath.Join(outputDir, vm.Name) - if err := os.MkdirAll(vmOutputDir, 0755); err != nil { - return fmt.Errorf("creating VM output directory: %w", err) + // Write script content to temporary file + if _, err := tmpfile.Write(script); err != nil { + return fmt.Errorf("writing script to temporary file: %w", err) } - remoteScriptPath := "/tmp/show-tech.sh" + if err := tmpfile.Sync(); err != nil { + return fmt.Errorf("syncing temporary script file: %w", err) + } - // Create a temporary file for the script - tempFile, err := os.CreateTemp("", "show-tech-*.sh") + // Upload the script from temporary file + err = ssh.Scp(tmpfile.Name(), remoteScriptPath) if err != nil { - return fmt.Errorf("creating temporary file for script: %w", err) + return fmt.Errorf("uploading script to %s: %w", entryName, err) } - defer func() { - // Ensure the temporary file is removed even if an error occurs - if removeErr := os.Remove(tempFile.Name()); removeErr != nil { - fmt.Printf("error removing temporary file: %v\n", removeErr) - } - }() - // Write the script to the temporary file - if _, err := tempFile.Write(script); err != nil { - return fmt.Errorf("writing script to temporary file: %w", err) + // Make script executable and run it + chmodCmd := fmt.Sprintf("chmod +x %s && %s", remoteScriptPath, remoteScriptPath) + stdout, stderr, done, err := ssh.Run(chmodCmd, 60*time.Second) + if err != nil { + return fmt.Errorf("executing show-tech on %s: %w", entryName, err) //nolint:goerr113 } - // Close the temporary file - if err := tempFile.Close(); err != nil { - return fmt.Errorf("closing temporary file: %w", err) + if !done { + return fmt.Errorf("show-tech execution timed out on %s: stdout: %s, stderr: %s", entryName, stdout, stderr) //nolint:goerr113 } - // Upload the temporary script to the remote server - if err := c.uploadFile(sftpClient, tempFile.Name(), remoteScriptPath); err != nil { - return fmt.Errorf("uploading script file: %w", err) + if stderr != "" { + slog.Debug("show-tech execution produced stderr", + "entry", entryName, + "stderr", stderr) } - // Make the script executable on the remote server - if _, err := client.Run(fmt.Sprintf("chmod +x %s", remoteScriptPath)); err != nil { - return fmt.Errorf("making script executable: %w", err) + // Download the output file + err = ssh.Scp(remoteOutputPath, localOutputPath) + if err != nil { + return fmt.Errorf("downloading show-tech output for %s: %w", entryName, err) } - // Execute the script on the remote server - if output, err := client.Run(remoteScriptPath); err != nil { - return fmt.Errorf("executing script on %s: %w\nOutput: %s", vm.Name, err, string(output)) + slog.Info("Show tech collected successfully", "entry", entryName, "output", localOutputPath) + + return nil +} + +// Helper to create an SSH client for the given entry +func (c *Config) createSSHConfig(ctx context.Context, entryName string, entry VLABAccessInfo) (*easyssh.MakeConfig, error) { + sshKeyPath := filepath.Join(VLABDir, VLABSSHKeyFile) + + // Verify SSH key exists + if _, err := os.Stat(sshKeyPath); err != nil { + return nil, fmt.Errorf("SSH key not found at %s: %w", sshKeyPath, err) } - localFile := filepath.Join(outputDir, fmt.Sprintf("%s-show-tech.log", vm.Name)) - remoteFile := "/tmp/show-tech.log" - if err := c.downloadFile(sftpClient, remoteFile, localFile); err != nil { - return fmt.Errorf("downloading output file: %w", err) + if entry.SSHPort > 0 { + slog.Info("SSH using local port", "entry", entryName, "port", entry.SSHPort) + + return &easyssh.MakeConfig{ + User: "core", + Server: "127.0.0.1", + Port: fmt.Sprintf("%d", entry.SSHPort), + KeyPath: sshKeyPath, + Timeout: 60 * time.Second, + }, nil } - return nil -} + if entry.IsSwitch { + slog.Info("SSH through control node", "entry", entryName, "type", "switch") + swIP, err := c.getSwitchIP(ctx, entryName) + if err != nil { + return nil, fmt.Errorf("getting switch IP: %w", err) + } -func (c *Config) waitForSSH(ctx context.Context, vm VM, auth goph.Auth) (*goph.Client, error) { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context cancelled while waiting for SSH") //nolint:goerr113 - case <-ticker.C: - client, err := goph.NewConn(&goph.Config{ - User: "core", - Addr: "127.0.0.1", - Port: getSSHPort(vm.ID), - Auth: auth, - Timeout: 10 * time.Second, - Callback: ssh.InsecureIgnoreHostKey(), //nolint:gosec - }) - if err == nil { - return client, nil - } + // Get control node port with validation + controlPort := getSSHPort(0) + if controlPort == 0 { + return nil, fmt.Errorf("invalid control node port (0) for %s", entryName) //nolint:goerr113 } + + // Create SSH config with proxy through control node + return &easyssh.MakeConfig{ + User: "admin", + Server: swIP, + KeyPath: sshKeyPath, + Timeout: 60 * time.Second, + Proxy: easyssh.DefaultConfig{ + User: "core", + Server: "127.0.0.1", + Port: fmt.Sprintf("%d", controlPort), + KeyPath: sshKeyPath, + Timeout: 60 * time.Second, + }, + }, nil } + + return nil, fmt.Errorf("unsupported entry type for %s", entryName) //nolint:goerr113 } -func (c *Config) uploadFile(sftpClient *sftp.Client, localPath, remotePath string) error { - localFile, err := os.Open(localPath) +// Helper to get switch IP using Kubernetes client +func (c *Config) getSwitchIP(ctx context.Context, entryName string) (string, error) { + kubeconfig := filepath.Join(c.WorkDir, VLABDir, VLABKubeConfig) + kube, err := kubeutil.NewClientWithCache(ctx, kubeconfig, wiringapi.SchemeBuilder) if err != nil { - return fmt.Errorf("opening local file %s: %w", localPath, err) + return "", fmt.Errorf("creating kube client: %w", err) } - defer localFile.Close() - remoteFile, err := sftpClient.Create(remotePath) - if err != nil { - return fmt.Errorf("creating remote file %s: %w", remotePath, err) + sw := &wiringapi.Switch{} + if err := kube.Get(ctx, client.ObjectKey{Name: entryName, Namespace: metav1.NamespaceDefault}, sw); err != nil { + return "", fmt.Errorf("getting switch object: %w", err) //nolint:goerr113 } - defer remoteFile.Close() - if _, err := io.Copy(remoteFile, localFile); err != nil { - return fmt.Errorf("copying file to remote: %w", err) + if sw.Spec.IP == "" { + return "", fmt.Errorf("switch IP not found: %s", entryName) //nolint:goerr113 } - return nil -} - -func (c *Config) downloadFile(sftpClient *sftp.Client, remotePath, localPath string) error { - remoteFile, err := sftpClient.Open(remotePath) + swIP, err := netip.ParsePrefix(sw.Spec.IP) if err != nil { - return fmt.Errorf("opening remote file %s: %w", remotePath, err) + return "", fmt.Errorf("parsing switch IP: %w", err) } - defer remoteFile.Close() - localFile, err := os.Create(localPath) - if err != nil { - return fmt.Errorf("creating local file %s: %w", localPath, err) - } - defer localFile.Close() + return swIP.Addr().String(), nil +} - if _, err := io.Copy(localFile, remoteFile); err != nil { - return fmt.Errorf("copying file from remote: %w", err) +// Helper to determine VM type based on entry +func getVMType(entry VLABAccessInfo) VMType { + switch { + case entry.IsSwitch: + return VMTypeSwitch + case entry.IsControl: + return VMTypeControl + case entry.IsServer: + return VMTypeServer + default: + return "" } - - return nil }