diff --git a/cmd/lock/lock.go b/cmd/lock/lock.go index 74f3c8d7..e5ce4e54 100755 --- a/cmd/lock/lock.go +++ b/cmd/lock/lock.go @@ -7,7 +7,9 @@ package lock import ( "fmt" "os" + "path/filepath" "perfspect/internal/common" + "perfspect/internal/progress" "perfspect/internal/report" "perfspect/internal/script" "perfspect/internal/target" @@ -165,30 +167,31 @@ func formalizeOutputFormat(outputFormat []string) []string { return result } -func pullDataFiles(appContext common.AppContext, scriptOutputs map[string]script.ScriptOutput, myTarget target.Target) error { - // if target is RawTarget, the scriptOutputs may retrieved from a input file, which hints we should not be able to pull the - // perf archive file. In this situation, we don't treat it as an error. - _, isRawTarget := myTarget.(*target.RawTarget) - if isRawTarget { - return nil - } - +func pullDataFiles(appContext common.AppContext, scriptOutputs map[string]script.ScriptOutput, myTarget target.Target, statusUpdate progress.MultiSpinnerUpdateFunc) error { localOutputDir := appContext.OutputDir tableValues := report.GetValuesForTable(report.KernelLockAnalysisTableName, scriptOutputs) + found := false for _, field := range tableValues.Fields { if field.Name == "Perf Package Path" { for _, remoteFile := range field.Values { - if len(remoteFile) == 0 { + if remoteFile == "" { continue } + found = true + _ = statusUpdate(myTarget.GetName(), "retrieving lock package") err := myTarget.PullFile(remoteFile, localOutputDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) + _ = statusUpdate(myTarget.GetName(), fmt.Sprintf("failed to retrieve lock package: %v", err)) return err } + _ = statusUpdate(myTarget.GetName(), fmt.Sprintf("retrieved lock package (%s)", filepath.Base(remoteFile))) } + break } } + if !found { + _ = statusUpdate(myTarget.GetName(), "no lock package found") + } return nil } diff --git a/internal/common/common.go b/internal/common/common.go index 0a39c7f1..4e19e50a 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -43,14 +43,11 @@ type FlagGroup struct { } type TargetScriptOutputs struct { - target target.Target + targetName string scriptOutputs map[string]script.ScriptOutput tableNames []string } -func (tso *TargetScriptOutputs) GetTarget() target.Target { - return tso.target -} func (tso *TargetScriptOutputs) GetScriptOutputs() map[string]script.ScriptOutput { return tso.scriptOutputs } @@ -83,7 +80,7 @@ const ( type SummaryFunc func([]report.TableValues, map[string]script.ScriptOutput) report.TableValues type InsightsFunc SummaryFunc -type AdhocFunc func(AppContext, map[string]script.ScriptOutput, target.Target) error +type AdhocFunc func(AppContext, map[string]script.ScriptOutput, target.Target, progress.MultiSpinnerUpdateFunc) error type ReportingCommand struct { Cmd *cobra.Command @@ -119,6 +116,7 @@ func (rc *ReportingCommand) Run() error { }() var orderedTargetScriptOutputs []TargetScriptOutputs + var myTargets []target.Target if FlagInput != "" { var err error orderedTargetScriptOutputs, err = outputsFromInput(rc.SummaryTableName) @@ -130,7 +128,9 @@ func (rc *ReportingCommand) Run() error { } } else { // get the targets - myTargets, targetErrs, err := GetTargets(rc.Cmd, elevatedPrivilegesRequired(rc.TableNames), false, localTempDir) + var targetErrs []error + var err error + myTargets, targetErrs, err = GetTargets(rc.Cmd, elevatedPrivilegesRequired(rc.TableNames), false, localTempDir) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) slog.Error(err.Error()) @@ -230,6 +230,36 @@ func (rc *ReportingCommand) Run() error { for _, reportFilePath := range reportFilePaths { fmt.Printf(" %s\n", reportFilePath) } + // lastly, run any adhoc actions + if rc.AdhocFunc != nil { + fmt.Println() + // setup and start the progress indicator + multiSpinner := progress.NewMultiSpinner() + for _, target := range myTargets { + err := multiSpinner.AddSpinner(target.GetName()) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + slog.Error(err.Error()) + rc.Cmd.SilenceUsage = true + return err + } + } + multiSpinner.Start() + adhocErrorChannel := make(chan error) + for i, t := range myTargets { + go func(target target.Target, i int) { + err := rc.AdhocFunc(appContext, orderedTargetScriptOutputs[i].scriptOutputs, target, multiSpinner.Status) + adhocErrorChannel <- err + }(t, i) + } + // wait for all adhoc actions to complete, errors were reported by the AdhocFunc + for range myTargets { + <-adhocErrorChannel + } + // stop the progress indicator + multiSpinner.Finish() + fmt.Println() + } return nil } @@ -266,7 +296,7 @@ func DefaultInsightsFunc(allTableValues []report.TableValues, scriptOutputs map[ // createRawReports creates the raw report(s) from the collected data func (rc *ReportingCommand) createRawReports(appContext AppContext, orderedTargetScriptOutputs []TargetScriptOutputs) error { for _, targetScriptOutputs := range orderedTargetScriptOutputs { - reportBytes, err := report.CreateRawReport(rc.TableNames, targetScriptOutputs.scriptOutputs, targetScriptOutputs.target.GetName()) + reportBytes, err := report.CreateRawReport(rc.TableNames, targetScriptOutputs.scriptOutputs, targetScriptOutputs.targetName) if err != nil { err = fmt.Errorf("failed to create raw report: %w", err) return err @@ -275,7 +305,7 @@ func (rc *ReportingCommand) createRawReports(appContext AppContext, orderedTarge if rc.ReportNamePost != "" { post = "_" + rc.ReportNamePost } - reportFilename := fmt.Sprintf("%s%s.%s", targetScriptOutputs.target.GetName(), post, "raw") + reportFilename := fmt.Sprintf("%s%s.%s", targetScriptOutputs.targetName, post, "raw") reportPath := filepath.Join(appContext.OutputDir, reportFilename) if err = writeReport(reportBytes, reportPath); err != nil { err = fmt.Errorf("failed to write report: %w", err) @@ -324,13 +354,6 @@ func (rc *ReportingCommand) createReports(appContext AppContext, orderedTargetSc insightsTableValues := rc.InsightsFunc(allTableValues, targetScriptOutputs.scriptOutputs) allTableValues = append(allTableValues, insightsTableValues) } - // special case - do some adhoc actions - if rc.AdhocFunc != nil { - err = rc.AdhocFunc(appContext, targetScriptOutputs.scriptOutputs, targetScriptOutputs.target) - if err != nil { - return nil, err - } - } // special case - add tableValues for the application version allTableValues = append(allTableValues, report.TableValues{ TableDefinition: report.TableDefinition{ @@ -342,20 +365,20 @@ func (rc *ReportingCommand) createReports(appContext AppContext, orderedTargetSc }) // create the report(s) for _, format := range formats { - reportBytes, err := report.Create(format, allTableValues, targetScriptOutputs.scriptOutputs, targetScriptOutputs.target.GetName()) + reportBytes, err := report.Create(format, allTableValues, targetScriptOutputs.scriptOutputs, targetScriptOutputs.targetName) if err != nil { err = fmt.Errorf("failed to create report: %w", err) return nil, err } if len(formats) == 1 && format == report.FormatTxt { - fmt.Printf("%s:\n", targetScriptOutputs.target.GetName()) + fmt.Printf("%s:\n", targetScriptOutputs.targetName) fmt.Print(string(reportBytes)) } post := "" if rc.ReportNamePost != "" { post = "_" + rc.ReportNamePost } - reportFilename := fmt.Sprintf("%s%s.%s", targetScriptOutputs.target.GetName(), post, format) + reportFilename := fmt.Sprintf("%s%s.%s", targetScriptOutputs.targetName, post, format) reportPath := filepath.Join(appContext.OutputDir, reportFilename) if err = writeReport(reportBytes, reportPath); err != nil { err = fmt.Errorf("failed to write report: %w", err) @@ -371,7 +394,7 @@ func (rc *ReportingCommand) createReports(appContext AppContext, orderedTargetSc // - only those that we received output from targetNames := make([]string, 0) for _, targetScriptOutputs := range orderedTargetScriptOutputs { - targetNames = append(targetNames, targetScriptOutputs.target.GetName()) + targetNames = append(targetNames, targetScriptOutputs.targetName) } multiTargetFormats := []string{report.FormatHtml, report.FormatXlsx} for _, format := range multiTargetFormats { @@ -413,8 +436,7 @@ func outputsFromInput(summaryTableName string) ([]TargetScriptOutputs, error) { } tableNames = util.UniqueAppend(tableNames, tableName) } - rawTarget := target.NewRawTarget(rawReport.TargetName) - orderedTargetScriptOutputs = append(orderedTargetScriptOutputs, TargetScriptOutputs{target: rawTarget, scriptOutputs: rawReport.ScriptOutputs, tableNames: tableNames}) + orderedTargetScriptOutputs = append(orderedTargetScriptOutputs, TargetScriptOutputs{targetName: rawReport.TargetName, scriptOutputs: rawReport.ScriptOutputs, tableNames: tableNames}) } return orderedTargetScriptOutputs, nil } @@ -467,7 +489,7 @@ func outputsFromTargets(cmd *cobra.Command, myTargets []target.Target, tableName // reorder to match order of myTargets for targetIdx, target := range myTargets { for _, targetScriptOutputs := range allTargetScriptOutputs { - if targetScriptOutputs.target.GetName() == target.GetName() { + if targetScriptOutputs.targetName == target.GetName() { targetScriptOutputs.tableNames = targetTableNames[targetIdx] orderedTargetScriptOutputs = append(orderedTargetScriptOutputs, targetScriptOutputs) break @@ -515,5 +537,5 @@ func collectOnTarget(myTarget target.Target, scriptsToRun []script.ScriptDefinit if statusUpdate != nil { _ = statusUpdate(myTarget.GetName(), "collection complete") } - channelTargetScriptOutputs <- TargetScriptOutputs{target: myTarget, scriptOutputs: scriptOutputs} + channelTargetScriptOutputs <- TargetScriptOutputs{targetName: myTarget.GetName(), scriptOutputs: scriptOutputs} } diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go index 7e8ff63c..4e0c4b0c 100644 --- a/internal/report/table_defs.go +++ b/internal/report/table_defs.go @@ -1272,8 +1272,7 @@ func sstTFLPTableValues(outputs map[string]script.ScriptOutput) []Field { for i, line := range lines { // field names are in the header if i == 0 { - fieldNames := strings.SplitSeq(line, ",") - for fieldName := range fieldNames { + for fieldName := range strings.SplitSeq(line, ",") { fields = append(fields, Field{Name: fieldName + " (MHz)"}) } continue diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index f726fa7d..f3a5de41 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -1703,8 +1703,7 @@ func cveInfoFromOutput(outputs map[string]script.ScriptOutput) [][]string { /* "1,3-5,8" -> [1,3,4,5,8] */ func expandCPUList(cpuList string) (cpus []int) { if cpuList != "" { - tokens := strings.SplitSeq(cpuList, ",") - for token := range tokens { + for token := range strings.SplitSeq(cpuList, ",") { if strings.Contains(token, "-") { subTokens := strings.Split(token, "-") if len(subTokens) == 2 { @@ -1805,8 +1804,7 @@ func nicIRQMappingsFromOutput(outputs map[string]script.ScriptOutput) [][]string // which is : // we need to reverse it to : cpuIRQMappings := make(map[int][]int) - irqCPUPairs := strings.SplitSeq(nic.CPUAffinity, ";") - for pair := range irqCPUPairs { + for pair := range strings.SplitSeq(nic.CPUAffinity, ";") { if pair == "" { continue } diff --git a/internal/script/script.go b/internal/script/script.go index c60b7e9f..49d63260 100644 --- a/internal/script/script.go +++ b/internal/script/script.go @@ -363,8 +363,7 @@ func formMasterScript(targetTempDirectory string, parallelScripts []ScriptDefini // It returns a list of ScriptOutput objects, one for each script that was run. func parseMasterScriptOutput(masterScriptOutput string) (scriptOutputs []ScriptOutput) { // split output of master script into individual script outputs - outputs := strings.SplitSeq(masterScriptOutput, "<---------------------->\n") - for output := range outputs { + for output := range strings.SplitSeq(masterScriptOutput, "<---------------------->\n") { lines := strings.Split(output, "\n") if len(lines) < 4 { // minimum lines for a script output continue diff --git a/internal/target/helpers.go b/internal/target/helpers.go new file mode 100644 index 00000000..a0e993aa --- /dev/null +++ b/internal/target/helpers.go @@ -0,0 +1,293 @@ +package target + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "bufio" + "context" + "errors" + "fmt" + "log/slog" + "os/exec" + "strings" + "time" +) + +// installLkms attempts to install a list of Linux Kernel Modules (LKMs) on the target system. +// It requires elevated privileges to perform the installation. +// +// Parameters: +// - t: A Target interface that represents the system where the LKMs will be installed. +// The Target must support privilege elevation. +// - lkms: A slice of strings representing the names of the LKMs to be installed. +// +// Returns: +// - installedLkms: A slice of strings containing the names of the LKMs that were successfully installed. +// - err: An error if privilege elevation is not possible or if any other issue occurs. +func installLkms(t Target, lkms []string) (installedLkms []string, err error) { + if !t.CanElevatePrivileges() { + err = fmt.Errorf("can't elevate privileges; elevated privileges required to install lkms") + return + } + for _, lkm := range lkms { + slog.Debug("attempting to install kernel module", slog.String("lkm", lkm)) + _, _, _, err := t.RunCommand(exec.Command("modprobe", "--first-time", lkm), 10, true) + if err != nil { + slog.Debug("kernel module already installed or problem installing", slog.String("lkm", lkm), slog.String("error", err.Error())) + continue + } + slog.Debug("kernel module installed", slog.String("lkm", lkm)) + installedLkms = append(installedLkms, lkm) + } + return +} + +// uninstallLkms attempts to uninstall a list of Linux kernel modules (LKMs) from the target system. +// It requires elevated privileges to perform the operation. +// +// Parameters: +// - t: A Target interface that represents the system where the LKMs will be uninstalled. +// The Target must support privilege elevation. +// - lkms: A slice of strings representing the names of the kernel modules to be uninstalled. +// +// Returns: +// - err: An error if privilege elevation is not possible or if any other issue occurs during the process. +func uninstallLkms(t Target, lkms []string) (err error) { + if !t.CanElevatePrivileges() { + err = fmt.Errorf("can't elevate privileges; elevated privileges required to uninstall lkms") + return + } + for _, lkm := range lkms { + slog.Debug("attempting to uninstall kernel module", slog.String("lkm", lkm)) + _, _, _, err := t.RunCommand(exec.Command("modprobe", "-r", lkm), 10, true) + if err != nil { + slog.Error("error uninstalling kernel module", slog.String("lkm", lkm), slog.String("error", err.Error())) + continue + } + slog.Debug("kernel module uninstalled", slog.String("lkm", lkm)) + } + return +} + +// runLocalCommandWithInputWithTimeout executes a local command with optional input and a timeout. +// It captures the command's standard output, standard error, and exit code. +// +// Parameters: +// - cmd: The command to execute, represented as an *exec.Cmd. +// - input: A string to be passed as input to the command's standard input. +// - timeout: The timeout in seconds for the command execution. If set to 0, no timeout is applied. +// +// Returns: +// - stdout: The standard output of the command as a string. +// - stderr: The standard error of the command as a string. +// - exitCode: The exit code of the command. If the command fails to execute, this may be undefined. +// - err: An error object if the command fails to execute or times out. +func runLocalCommandWithInputWithTimeout(cmd *exec.Cmd, input string, timeout int) (stdout string, stderr string, exitCode int, err error) { + logInput := "" + if input != "" { + logInput = "******" + } + slog.Debug("running local command", slog.String("cmd", cmd.String()), slog.String("input", logInput), slog.Int("timeout", timeout)) + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + commandWithContext := exec.CommandContext(ctx, cmd.Path, cmd.Args[1:]...) // nosemgrep + commandWithContext.Env = cmd.Env + cmd = commandWithContext + } + if input != "" { + cmd.Stdin = strings.NewReader(input) + } + var outbuf, errbuf strings.Builder + cmd.Stdout = &outbuf + cmd.Stderr = &errbuf + err = cmd.Run() + stdout = outbuf.String() + stderr = errbuf.String() + if err != nil { + exitError := &exec.ExitError{} + if errors.As(err, &exitError) { + exitCode = exitError.ExitCode() + } + } + return +} + +// runLocalCommandWithInputWithTimeoutAsync executes a local command asynchronously with optional input and timeout. +// It streams the command's stdout and stderr to the provided channels and sends the exit code to the exitcodeChannel. +// +// Parameters: +// - cmd: The command to execute, represented as an *exec.Cmd. +// - stdoutChannel: A channel to send lines of stdout output. +// - stderrChannel: A channel to send lines of stderr output. +// - exitcodeChannel: A channel to send the exit code of the command. +// - input: A string to be passed as input to the command's stdin. If empty, no input is provided. +// - timeout: The timeout in seconds for the command execution. If 0 or less, no timeout is applied. +// +// Returns: +// - err: An error if the command fails to start or if there are issues with pipes. +func runLocalCommandWithInputWithTimeoutAsync(cmd *exec.Cmd, stdoutChannel chan string, stderrChannel chan string, exitcodeChannel chan int, input string, timeout int) (err error) { + logInput := "" + if input != "" { + logInput = "******" + } + slog.Debug("running local command (async)", slog.String("cmd", cmd.String()), slog.String("input", logInput), slog.Int("timeout", timeout)) + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + commandWithContext := exec.CommandContext(ctx, cmd.Path, cmd.Args[1:]...) // nosemgrep + commandWithContext.Env = cmd.Env + cmd = commandWithContext + } + if input != "" { + cmd.Stdin = strings.NewReader(input) + } + stdoutReader, err := cmd.StdoutPipe() + if err != nil { + err = fmt.Errorf("failed to get stdout pipe: %v", err) + return + } + stdoutScanner := bufio.NewScanner(stdoutReader) + stderrReader, err := cmd.StderrPipe() + if err != nil { + err = fmt.Errorf("failed to get stderr pipe: %v", err) + return + } + stderrScanner := bufio.NewScanner(stderrReader) + if err = cmd.Start(); err != nil { + err = fmt.Errorf("failed to run command (%s): %v", cmd, err) + return + } + go func() { + for stdoutScanner.Scan() { + text := stdoutScanner.Text() + stdoutChannel <- text + } + }() + go func() { + for stderrScanner.Scan() { + text := stderrScanner.Text() + stderrChannel <- text + } + }() + err = cmd.Wait() + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + exitcodeChannel <- exitError.ExitCode() + } else { + slog.Error("unexpected error type while waiting for command to finish", slog.String("cmd", cmd.String()), slog.String("error", err.Error())) + exitcodeChannel <- -1 + } + } else { + exitcodeChannel <- 0 + } + return nil +} + +// getArchitecture determines the architecture of the target system by executing +// the "uname -m" command. It returns the architecture as a string and an error +// if the command execution fails. +// +// Parameters: +// - t: A Target instance that provides the ability to run commands on the target system. +// +// Returns: +// - arch: A string representing the architecture of the target system (e.g., "x86_64"). +// - err: An error if the command execution fails or if there is an issue retrieving the architecture. +func getArchitecture(t Target) (arch string, err error) { + cmd := exec.Command("uname", "-m") + arch, _, _, err = t.RunCommand(cmd, 0, true) + if err != nil { + return + } + arch = strings.TrimSpace(arch) + return +} + +// getFamily retrieves the CPU family of the target system by executing a shell command. +// It runs the "lscpu" command to extract the "CPU family" field and returns the value as a string. +// +// Parameters: +// - t: A Target instance that provides the method to execute the command. +// +// Returns: +// - family: A string representing the CPU family of the target system. +// - err: An error if the command execution or parsing fails. +func getFamily(t Target) (family string, err error) { + cmd := exec.Command("bash", "-c", "lscpu | grep -i \"^CPU family:\" | awk '{print $NF}'") + family, _, _, err = t.RunCommand(cmd, 0, true) + if err != nil { + return + } + family = strings.TrimSpace(family) + return +} + +// getModel retrieves the CPU model of the target system by executing a shell command. +// It runs the "lscpu" command, filters the output for the "Model" field, and extracts +// the last field of the line using "awk". The result is trimmed of any leading or trailing +// whitespace before being returned. +// +// Parameters: +// +// t - The Target interface that provides the ability to execute commands on the target system. +// +// Returns: +// +// model - A string representing the CPU model of the target system. +// err - An error if the command execution fails or if there is an issue retrieving the model. +func getModel(t Target) (model string, err error) { + cmd := exec.Command("bash", "-c", "lscpu | grep -i model: | awk '{print $NF}'") + model, _, _, err = t.RunCommand(cmd, 0, true) + if err != nil { + return + } + model = strings.TrimSpace(model) + return +} + +// getStepping retrieves the CPU stepping information of the target system. +// It executes a shell command to parse the output of the `lscpu` command +// and extracts the stepping value using `grep` and `awk`. +// +// Parameters: +// - t: A Target instance that provides the ability to execute commands. +// +// Returns: +// - stepping: A string representing the CPU stepping value. +// - err: An error if the command execution or parsing fails. +func getStepping(t Target) (stepping string, err error) { + cmd := exec.Command("bash", "-c", "lscpu | grep -i stepping: | awk '{print $NF}'") + stepping, _, _, err = t.RunCommand(cmd, 0, true) + if err != nil { + return + } + stepping = strings.TrimSpace(stepping) + return +} + +// getVendor retrieves the vendor ID of the CPU by executing a shell command. +// It runs the "lscpu" command, filters the output for the "Vendor ID" field, +// and extracts the last field using "awk". The result is then trimmed of any +// leading or trailing whitespace. +// +// Parameters: +// +// t Target - The target object that provides the RunCommand method. +// +// Returns: +// +// vendor (string) - The vendor ID of the CPU. +// err (error) - An error if the command execution or parsing fails. +func getVendor(t Target) (vendor string, err error) { + cmd := exec.Command("bash", "-c", "lscpu | grep -i \"^Vendor ID:\" | awk '{print $NF}'") + vendor, _, _, err = t.RunCommand(cmd, 0, true) + if err != nil { + return + } + vendor = strings.TrimSpace(vendor) + return +} diff --git a/internal/target/local_target.go b/internal/target/local_target.go new file mode 100644 index 00000000..e5b902e6 --- /dev/null +++ b/internal/target/local_target.go @@ -0,0 +1,268 @@ +package target + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "io" + "log/slog" + "os" + "os/exec" + "path/filepath" + "perfspect/internal/util" + "strings" +) + +// SetSudo (LocalTarget only) sets the sudo password for the target. +// Also sets the canElevate field to 0 to indicate that the sudo password has not been verified. +func (t *LocalTarget) SetSudo(sudo string) { + t.sudo = sudo + t.canElevate = 0 +} + +// RunCommand executes the given command with a timeout and returns the standard output, +// standard error, exit code, and any error that occurred. +func (t *LocalTarget) RunCommand(cmd *exec.Cmd, timeout int, argNotUsed bool) (stdout string, stderr string, exitCode int, err error) { + input := "" + if t.sudo != "" && len(cmd.Args) > 2 && cmd.Args[0] == "sudo" && strings.HasPrefix(cmd.Args[1], "-") && strings.Contains(cmd.Args[1], "S") { // 'sudo -S' gets password from stdin + input = t.sudo + "\n" + } + return runLocalCommandWithInputWithTimeout(cmd, input, timeout) +} + +// RunCommandAsync runs the given command asynchronously on the target. +// It sends the command to the cmdChannel and executes it with a timeout. +// The output from the command is sent to the stdoutChannel and stderrChannel, +// and the exit code is sent to the exitcodeChannel. +// The timeout parameter specifies the maximum time allowed for the command to run. +// Returns an error if there was a problem running the command. +func (t *LocalTarget) RunCommandAsync(cmd *exec.Cmd, timeout int, argNotUsed bool, stdoutChannel chan string, stderrChannel chan string, exitcodeChannel chan int, cmdChannel chan *exec.Cmd) (err error) { + localCommand := cmd + cmdChannel <- localCommand + err = runLocalCommandWithInputWithTimeoutAsync(localCommand, stdoutChannel, stderrChannel, exitcodeChannel, "", timeout) + return +} + +func (t *LocalTarget) GetArchitecture() (string, error) { + var err error + if t.arch == "" { + t.arch, err = getArchitecture(t) + } + return t.arch, err +} + +func (t *LocalTarget) GetFamily() (string, error) { + var err error + if t.family == "" { + t.family, err = getFamily(t) + } + return t.family, err +} + +func (t *LocalTarget) GetModel() (string, error) { + var err error + if t.model == "" { + t.model, err = getModel(t) + } + return t.model, err +} + +func (t *LocalTarget) GetStepping() (string, error) { + var err error + if t.stepping == "" { + t.stepping, err = getStepping(t) + } + return t.stepping, err +} + +func (t *LocalTarget) GetVendor() (string, error) { + var err error + if t.vendor == "" { + t.vendor, err = getVendor(t) + } + return t.vendor, err +} + +// CreateTempDirectory creates a temporary directory under the specified root directory. +// If the root directory is not specified, the temporary directory will be created in the current directory. +// It returns the path of the created temporary directory and any error encountered. +func (t *LocalTarget) CreateTempDirectory(rootDir string) (tempDir string, err error) { + if t.tempDir != "" { + return t.tempDir, nil + } + temp, err := os.MkdirTemp(rootDir, "perfspect.tmp.") + if err != nil { + return + } + tempDir, err = util.AbsPath(temp) + if err != nil { + return + } + t.tempDir = tempDir + return +} + +// RemoveTempDirectory removes the temporary directory created by CreateTempDirectory. +func (t *LocalTarget) RemoveTempDirectory() (err error) { + if t.tempDir != "" { + err = t.RemoveDirectory(t.tempDir) + if err == nil { + t.tempDir = "" + } + } + return +} + +func (t *LocalTarget) GetTempDirectory() string { + return t.tempDir +} + +// PushFile copies a file or directory from the source path to the destination path. +// If the source path points to a directory, it creates the corresponding directory +// at the destination and recursively copies its contents. If the source path points +// to a file, it directly copies the file to the destination. +// +// Parameters: +// - srcPath: The path to the source file or directory to be copied. +// - dstPath: The destination path where the file or directory should be copied. +// +// Returns: +// - err: An error if the operation fails, or nil if the operation succeeds. +func (t *LocalTarget) PushFile(srcPath string, dstPath string) (err error) { + srcFileStat, err := os.Stat(srcPath) + if err != nil { + return + } + if srcFileStat.IsDir() { + newDstDir := filepath.Join(dstPath, filepath.Base(srcPath)) + err = util.CreateDirectoryIfNotExists(newDstDir, 0755) + if err != nil { + return + } + err = util.CopyDirectory(srcPath, newDstDir) + return + } + err = util.CopyFile(srcPath, dstPath) + return +} + +// PullFile copies a file from the source path on the local target to the destination directory. +// This function currently calls PushFile, which may not align with the intended behavior. +// +// Parameters: +// - srcPath: The path to the source file to be pulled. +// - dstDir: The destination directory where the file should be placed. +// +// Returns: +// - An error if the operation fails. +func (t *LocalTarget) PullFile(srcPath string, dstDir string) error { + return t.PushFile(srcPath, dstDir) +} + +// CreateDirectory creates a new directory under the specified base directory. +// It returns the full path of the created directory and any error encountered. +func (t *LocalTarget) CreateDirectory(baseDir string, targetDir string) (dir string, err error) { + dir = filepath.Join(baseDir, targetDir) + err = os.Mkdir(dir, 0764) + return +} + +// RemoveDirectory removes the specified target directory. +// If the target directory is not empty, it will be deleted along with all its contents. +// The method returns an error if any error occurs during the removal process. +func (t *LocalTarget) RemoveDirectory(targetDir string) (err error) { + if targetDir != "" { + err = os.RemoveAll(targetDir) + } + return +} + +// CanConnect checks if the local target can establish a connection (always true). +func (t *LocalTarget) CanConnect() bool { + return true +} + +// CanElevatePrivileges (on LocalTarget) checks if the user is root or sudo can be used to elevate privileges. +// It returns true if the user is root or if the sudo password works. +// If the `sudo` command is configured, it will attempt to run a command with sudo +// and check if the password works. If the passwordless sudo is configured, +// it will also check if passwordless sudo works. +// Returns true if the user can elevate privileges, false otherwise. +func (t *LocalTarget) CanElevatePrivileges() bool { + if t.canElevate != 0 { + return t.canElevate == 1 + } + if t.IsSuperUser() { + t.canElevate = 1 + return true // user is root + } + if t.sudo != "" { + cmd := exec.Command("sudo", "-kS", "ls") + stdin, _ := cmd.StdinPipe() + go func() { + defer stdin.Close() + _, err := io.WriteString(stdin, t.sudo+"\n") + if err != nil { + slog.Error("error writing sudo password", slog.String("error", err.Error())) + } + }() + _, _, _, err := t.RunCommand(cmd, 0, true) + if err == nil { + t.canElevate = 1 + return true // sudo password works + } + } + cmd := exec.Command("sudo", "-kS", "ls") + _, _, _, err := t.RunCommand(cmd, 0, true) + if err == nil { // true - passwordless sudo works + t.canElevate = 1 + return true + } + t.canElevate = -1 + return false +} + +// IsSuperUser checks if the current user is a superuser. +// It returns true if the user is a superuser, false otherwise. +func (t *LocalTarget) IsSuperUser() bool { + return os.Geteuid() == 0 +} + +// InstallLkms installs the specified LKMs (Loadable Kernel Modules) on the target. +// It returns the list of installed LKMs and any error encountered during the installation process. +func (t *LocalTarget) InstallLkms(lkms []string) (installedLkms []string, err error) { + return installLkms(t, lkms) +} + +// UninstallLkms uninstalls the specified LKMs (Loadable Kernel Modules) from the target. +// It takes a slice of strings representing the names of the LKMs to be uninstalled. +// It returns an error if any error occurs during the uninstallation process. +func (t *LocalTarget) UninstallLkms(lkms []string) (err error) { + return uninstallLkms(t, lkms) +} + +// GetName returns the name of the Target. +func (t *LocalTarget) GetName() (host string) { + return t.host +} + +// GetUserPath returns the user's PATH environment variable after verifying that it only contains valid paths. +// It checks each path in the PATH environment variable and filters out any non-path strings. +// The function returns the verified paths joined by ":" as a string. +func (t *LocalTarget) GetUserPath() (string, error) { + if t.userPath == "" { + // get user's PATH environment variable, verify that it only contains paths (mitigate risk raised by Checkmarx) + var verifiedPaths []string + pathEnv := os.Getenv("PATH") + for p := range strings.SplitSeq(pathEnv, ":") { + files, err := filepath.Glob(p) + // Goal is to filter out any non path strings + // Glob will throw an error on pattern mismatch and return no files if no files + if err == nil && len(files) > 0 { + verifiedPaths = append(verifiedPaths, p) + } + } + t.userPath = strings.Join(verifiedPaths, ":") + } + return t.userPath, nil +} diff --git a/internal/target/remote_target.go b/internal/target/remote_target.go new file mode 100644 index 00000000..b4fb6da2 --- /dev/null +++ b/internal/target/remote_target.go @@ -0,0 +1,406 @@ +package target + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// SetSshPassPath sets the path to the sshpass binary (RemoteTarget only). +func (t *RemoteTarget) SetSshPassPath(sshpassPath string) { + t.sshpassPath = sshpassPath +} + +// SetSshPass sets the ssh password for the target (RemoteTarget only). +func (t *RemoteTarget) SetSshPass(sshPass string) { + t.sshPass = sshPass +} + +// RunCommand executes a command on the remote target using SSH. It prepares the +// local command to be executed, optionally reusing an existing SSH connection, +// and runs it with a specified timeout. +// +// Parameters: +// - cmd: The command to be executed, represented as an *exec.Cmd. +// - timeout: The maximum duration (in seconds) to wait for the command to complete. +// - reuseSSHConnection: A boolean indicating whether to reuse an existing SSH connection. +// +// Returns: +// - stdout: The standard output of the executed command. +// - stderr: The standard error output of the executed command. +// - exitCode: The exit code returned by the command. +// - err: An error object if the command execution fails. +func (t *RemoteTarget) RunCommand(cmd *exec.Cmd, timeout int, reuseSSHConnection bool) (stdout string, stderr string, exitCode int, err error) { + localCommand := t.prepareLocalCommand(cmd, reuseSSHConnection) + return runLocalCommandWithInputWithTimeout(localCommand, "", timeout) +} + +// RunCommandAsync executes a command asynchronously on a remote target. +// It prepares the local command based on the provided parameters and runs it +// with a specified timeout. The function communicates the command's output, +// error, and exit code through the provided channels. +// +// Parameters: +// - cmd: The command to be executed, represented as an *exec.Cmd. +// - timeout: The maximum duration (in seconds) to allow the command to run. +// - reuseSSHConnection: A boolean indicating whether to reuse an existing SSH connection. +// - stdoutChannel: A channel to send the standard output of the command. +// - stderrChannel: A channel to send the standard error of the command. +// - exitcodeChannel: A channel to send the exit code of the command. +// - cmdChannel: A channel to send the prepared local command. +// +// Returns: +// - err: An error object if the command fails to execute or times out. +func (t *RemoteTarget) RunCommandAsync(cmd *exec.Cmd, timeout int, reuseSSHConnection bool, stdoutChannel chan string, stderrChannel chan string, exitcodeChannel chan int, cmdChannel chan *exec.Cmd) (err error) { + localCommand := t.prepareLocalCommand(cmd, reuseSSHConnection) + cmdChannel <- localCommand + err = runLocalCommandWithInputWithTimeoutAsync(localCommand, stdoutChannel, stderrChannel, exitcodeChannel, "", timeout) + return +} + +func (t *RemoteTarget) GetArchitecture() (string, error) { + var err error + if t.arch == "" { + t.arch, err = getArchitecture(t) + } + return t.arch, err +} + +func (t *RemoteTarget) GetFamily() (string, error) { + var err error + if t.family == "" { + t.family, err = getFamily(t) + } + return t.family, err +} + +func (t *RemoteTarget) GetModel() (string, error) { + var err error + if t.model == "" { + t.model, err = getModel(t) + } + return t.model, err +} + +func (t *RemoteTarget) GetStepping() (string, error) { + var err error + if t.stepping == "" { + t.stepping, err = getStepping(t) + } + return t.stepping, err +} + +func (t *RemoteTarget) GetVendor() (string, error) { + var err error + if t.vendor == "" { + t.vendor, err = getVendor(t) + } + return t.vendor, err +} + +// CreateTempDirectory creates a temporary directory on the remote target. +// If a temporary directory has already been created, it returns the existing one. +// The function takes an optional rootDir parameter to specify the root directory +// for the temporary directory. If rootDir is provided, it is passed as an argument +// to the "mktemp" command to set the base directory for the temporary directory. +// The function executes the "mktemp" command to create the directory and resolves +// its absolute path using "realpath". The resulting directory path is cached in +// the RemoteTarget instance for reuse. +// +// Parameters: +// - rootDir: An optional string specifying the root directory for the temporary directory. +// +// Returns: +// - tempDir: The absolute path of the created temporary directory. +// - err: An error if the temporary directory creation or command execution fails. +func (t *RemoteTarget) CreateTempDirectory(rootDir string) (tempDir string, err error) { + if t.tempDir != "" { + return t.tempDir, nil + } + var root string + if rootDir != "" { + root = fmt.Sprintf("--tmpdir=%s", rootDir) + } + cmd := exec.Command("mktemp", "-d", "-t", root, "perfspect.tmp.XXXXXXXXXX", "|", "xargs", "realpath") + tempDir, _, _, err = t.RunCommand(cmd, 0, true) + if err != nil { + return + } + tempDir = strings.TrimSpace(tempDir) + t.tempDir = tempDir + return +} + +func (t *RemoteTarget) RemoveTempDirectory() (err error) { + if t.tempDir != "" { + err = t.RemoveDirectory(t.tempDir) + if err == nil { + t.tempDir = "" + } + } + return +} + +// GetTempDirectory returns the path to the temporary directory associated with the RemoteTarget. +// This directory is used for storing temporary files during the target's operation. +func (t *RemoteTarget) GetTempDirectory() string { + return t.tempDir +} + +// PushFile transfers a file from the local system to a remote directory on the target. +// It uses SCP (Secure Copy Protocol) to perform the file transfer. +// +// Parameters: +// - srcPath: The path to the source file on the local system. +// - dstDir: The destination directory on the remote target. +// +// The function logs the operation details, including the source path, destination directory, +// standard output, standard error, and the exit code of the SCP command. +// +// Returns: +// - An error if the file transfer fails, or nil if the operation is successful. +func (t *RemoteTarget) PushFile(srcPath string, dstDir string) error { + stdout, stderr, exitCode, err := t.prepareAndRunSCPCommand(srcPath, dstDir, true) + slog.Debug("push file", slog.String("srcPath", srcPath), slog.String("dstDir", dstDir), slog.String("stdout", stdout), slog.String("stderr", stderr), slog.Int("exitCode", exitCode)) + return err +} + +// PullFile copies a file from a remote source path to a local destination directory +// using SCP (Secure Copy Protocol). It logs the operation details including the +// source path, destination directory, standard output, standard error, and exit code. +// +// Parameters: +// - srcPath: The path to the file on the remote system to be copied. +// - dstDir: The local directory where the file will be copied to. +// +// Returns: +// - error: An error object if the operation fails, or nil if the operation succeeds. +func (t *RemoteTarget) PullFile(srcPath string, dstDir string) error { + stdout, stderr, exitCode, err := t.prepareAndRunSCPCommand(srcPath, dstDir, false) + slog.Debug("pull file", slog.String("srcPath", srcPath), slog.String("dstDir", dstDir), slog.String("stdout", stdout), slog.String("stderr", stderr), slog.Int("exitCode", exitCode)) + return err +} + +func (t *RemoteTarget) CreateDirectory(baseDir string, targetDir string) (dir string, err error) { + dir = filepath.Join(baseDir, targetDir) + cmd := exec.Command("mkdir", dir) + _, _, _, err = t.RunCommand(cmd, 0, true) + return +} + +func (t *RemoteTarget) RemoveDirectory(targetDir string) (err error) { + if targetDir != "" { + cmd := exec.Command("rm", "-rf", targetDir) + _, _, _, err = t.RunCommand(cmd, 0, true) + } + return +} + +// CanConnect checks if the target is reachable. +func (t *RemoteTarget) CanConnect() bool { + cmd := exec.Command("exit", "0") + _, _, _, err := t.RunCommand(cmd, 5, true) + return err == nil +} + +// CanElevatePrivileges (on RemoteTarget) checks if the user name is root or if sudo can be used to elevate privileges. +// Note that the sudo password is not used for this check. Password-less sudo is required. +func (t *RemoteTarget) CanElevatePrivileges() bool { + if t.canElevate != 0 { + return t.canElevate == 1 + } + if t.IsSuperUser() { + t.canElevate = 1 + return true + } + cmd := exec.Command("sudo", "-kS", "ls") + _, _, _, err := t.RunCommand(cmd, 0, true) + if err == nil { // true - passwordless sudo works + t.canElevate = 1 + return true + } + t.canElevate = -1 + return false +} + +func (t *RemoteTarget) IsSuperUser() bool { + return t.user == "root" +} + +func (t *RemoteTarget) InstallLkms(lkms []string) (installedLkms []string, err error) { + return installLkms(t, lkms) +} + +func (t *RemoteTarget) UninstallLkms(lkms []string) (err error) { + return uninstallLkms(t, lkms) +} + +func (t *RemoteTarget) GetName() (host string) { + if t.name == "" { + return t.host + } + return t.name +} + +func (t *RemoteTarget) GetUserPath() (string, error) { + if t.userPath == "" { + cmd := exec.Command("echo", "$PATH") + stdout, _, _, err := t.RunCommand(cmd, 0, true) + if err != nil { + return "", err + } + t.userPath = strings.TrimSpace(stdout) + } + return t.userPath, nil +} + +func (t *RemoteTarget) prepareSSHFlags(scp bool, useControlMaster bool, prompt bool) (flags []string) { + flags = []string{ + "-2", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "StrictHostKeyChecking=no", + "-o", + "ConnectTimeout=10", // This one exposes a bug in Windows' SSH client. Each connection takes + "-o", // 10 seconds to establish. https://github.com/PowerShell/Win32-OpenSSH/issues/1352 + "GSSAPIAuthentication=no", // This one is not supported, but is ignored on Windows. + "-o", + "ServerAliveInterval=30", + "-o", + "ServerAliveCountMax=10", // 30 * 10 = maximum 300 seconds before disconnect on no data + "-o", + "LogLevel=ERROR", + } + // turn on batch mode to avoid prompts for passwords + if !prompt { + promptFlags := []string{ + "-o", + "BatchMode=yes", + } + flags = append(flags, promptFlags...) + } + // when using a control master, a long-running remote program doesn't get terminated when the local ssh client is terminated + if useControlMaster { + controlPathFlags := []string{ + "-o", + "ControlPath=" + filepath.Join(os.TempDir(), fmt.Sprintf("control-%%h-%%p-%%r-%d", os.Getpid())), + "-o", + "ControlMaster=auto", + "-o", + "ControlPersist=1m", + } + flags = append(flags, controlPathFlags...) + } + if t.key != "" { + keyFlags := []string{ + "-o", + "PreferredAuthentications=publickey", + "-o", + "PasswordAuthentication=no", + "-i", + t.key, + } + flags = append(flags, keyFlags...) + } + if t.port != "" { + if scp { + flags = append(flags, "-P") + } else { + flags = append(flags, "-p") + } + flags = append(flags, t.port) + } + return +} + +func (t *RemoteTarget) prepareSSHCommand(command []string, useControlMaster bool, prompt bool) []string { + var cmd []string + cmd = append(cmd, "ssh") + cmd = append(cmd, t.prepareSSHFlags(false, useControlMaster, prompt)...) + if t.user != "" { + cmd = append(cmd, t.user+"@"+t.host) + } else { + cmd = append(cmd, t.host) + } + cmd = append(cmd, "--") + cmd = append(cmd, command...) + return cmd +} + +func (t *RemoteTarget) prepareSCPCommand(src string, dstDir string, push bool) []string { + var cmd []string + cmd = append(cmd, "scp") + cmd = append(cmd, t.prepareSSHFlags(true, true, false)...) + if push { + fileInfo, err := os.Stat(src) + if err != nil { + slog.Error("error getting file info", slog.String("src", src), slog.String("error", err.Error())) + return nil + } + if fileInfo.IsDir() { + cmd = append(cmd, "-r") + } + cmd = append(cmd, src) + dst := t.host + ":" + dstDir + if t.user != "" { + dst = t.user + "@" + dst + } + cmd = append(cmd, dst) + } else { // pull + s := t.host + ":" + src + if t.user != "" { + s = t.user + "@" + s + } + cmd = append(cmd, s) + cmd = append(cmd, dstDir) + } + return cmd +} + +func (t *RemoteTarget) prepareLocalCommand(cmd *exec.Cmd, useControlMaster bool) *exec.Cmd { + var name string + var args []string + usePass := t.key == "" && t.sshPass != "" + sshCommand := t.prepareSSHCommand(cmd.Args, useControlMaster, usePass) + if usePass { + name = t.sshpassPath + args = []string{"-e", "--"} + args = append(args, sshCommand...) + } else { + name = sshCommand[0] + args = sshCommand[1:] + } + localCommand := exec.Command(name, args...) // nosemgrep + if usePass { + localCommand.Env = append(localCommand.Env, "SSHPASS="+t.sshPass) + } + return localCommand +} + +func (t *RemoteTarget) prepareAndRunSCPCommand(srcPath string, dstDir string, isPush bool) (stdout string, stderr string, exitCode int, err error) { + scpCommand := t.prepareSCPCommand(srcPath, dstDir, isPush) + var name string + var args []string + usePass := t.key == "" && t.sshPass != "" + if usePass { + name = t.sshpassPath + args = append(args, "-e", "--") + args = append(args, scpCommand...) + } else { + name = scpCommand[0] + args = scpCommand[1:] + } + localCommand := exec.Command(name, args...) // nosemgrep + if usePass { + localCommand.Env = append(localCommand.Env, "SSHPASS="+t.sshPass) + } + stdout, stderr, exitCode, err = runLocalCommandWithInputWithTimeout(localCommand, "", 0) + return +} diff --git a/internal/target/target.go b/internal/target/target.go index 2d71b740..63819ab8 100644 --- a/internal/target/target.go +++ b/internal/target/target.go @@ -7,19 +7,8 @@ package target // SPDX-License-Identifier: BSD-3-Clause import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "log/slog" "os" "os/exec" - "path/filepath" - "strings" - "time" - - "perfspect/internal/util" ) // Target represents a machine or system where commands can be run. @@ -124,20 +113,25 @@ type Target interface { UninstallLkms(lkms []string) error } -type LocalTarget struct { - host string - sudo string +type BaseTarget struct { tempDir string + canElevate int // zero indicates unknown, 1 indicates yes, -1 indicates no arch string family string model string stepping string - userPath string - canElevate int // zero indicates unknown, 1 indicates yes, -1 indicates no vendor string + userPath string +} + +type LocalTarget struct { + BaseTarget + host string + sudo string } type RemoteTarget struct { + BaseTarget name string host string port string @@ -145,21 +139,11 @@ type RemoteTarget struct { key string sshPass string sshpassPath string - tempDir string - arch string - family string - model string - stepping string - userPath string - canElevate int - vendor string -} - -type RawTarget struct { - name string } -// NewLocalTarget creates a new LocalTarget +// NewLocalTarget creates a new LocalTarget. +// It initializes the host name to the local machine's hostname. +// If the hostname cannot be retrieved, it defaults to "localhost". func NewLocalTarget() *LocalTarget { hostName, err := os.Hostname() if err != nil { @@ -183,886 +167,3 @@ func NewRemoteTarget(name string, host string, port string, user string, key str } return t } - -// NewRawTarget creates a new RawTarget instance with the provided parameters. -// This could be useful as when use file as input but no actual target available -func NewRawTarget(name string) *RawTarget { - t := &RawTarget{ - name: name, - } - return t -} - -// SetSudo sets the sudo password for the target (LocalTarget only). -// Also sets the canElevate field to 0 to indicate that the sudo password has not been verified. -func (t *LocalTarget) SetSudo(sudo string) { - t.sudo = sudo - t.canElevate = 0 -} - -// SetSshPassPath sets the path to the sshpass binary (RemoteTarget only). -func (t *RemoteTarget) SetSshPassPath(sshpassPath string) { - t.sshpassPath = sshpassPath -} - -// SetSshPass sets the ssh password for the target (RemoteTarget only). -func (t *RemoteTarget) SetSshPass(sshPass string) { - t.sshPass = sshPass -} - -// RunCommand executes the given command with a timeout and returns the standard output, -// standard error, exit code, and any error that occurred. -func (t *LocalTarget) RunCommand(cmd *exec.Cmd, timeout int, argNotUsed bool) (stdout string, stderr string, exitCode int, err error) { - input := "" - if t.sudo != "" && len(cmd.Args) > 2 && cmd.Args[0] == "sudo" && strings.HasPrefix(cmd.Args[1], "-") && strings.Contains(cmd.Args[1], "S") { // 'sudo -S' gets password from stdin - input = t.sudo + "\n" - } - return runLocalCommandWithInputWithTimeout(cmd, input, timeout) -} - -func (t *RemoteTarget) RunCommand(cmd *exec.Cmd, timeout int, reuseSSHConnection bool) (stdout string, stderr string, exitCode int, err error) { - localCommand := t.prepareLocalCommand(cmd, reuseSSHConnection) - return runLocalCommandWithInputWithTimeout(localCommand, "", timeout) -} - -// RunCommandAsync runs the given command asynchronously on the target. -// It sends the command to the cmdChannel and executes it with a timeout. -// The output from the command is sent to the stdoutChannel and stderrChannel, -// and the exit code is sent to the exitcodeChannel. -// The timeout parameter specifies the maximum time allowed for the command to run. -// Returns an error if there was a problem running the command. -func (t *LocalTarget) RunCommandAsync(cmd *exec.Cmd, timeout int, argNotUsed bool, stdoutChannel chan string, stderrChannel chan string, exitcodeChannel chan int, cmdChannel chan *exec.Cmd) (err error) { - localCommand := cmd - cmdChannel <- localCommand - err = runLocalCommandWithInputWithTimeoutAsync(localCommand, stdoutChannel, stderrChannel, exitcodeChannel, "", timeout) - return -} - -func (t *RemoteTarget) RunCommandAsync(cmd *exec.Cmd, timeout int, reuseSSHConnection bool, stdoutChannel chan string, stderrChannel chan string, exitcodeChannel chan int, cmdChannel chan *exec.Cmd) (err error) { - localCommand := t.prepareLocalCommand(cmd, reuseSSHConnection) - cmdChannel <- localCommand - err = runLocalCommandWithInputWithTimeoutAsync(localCommand, stdoutChannel, stderrChannel, exitcodeChannel, "", timeout) - return -} - -// GetArchitecture returns the architecture of the target. -// It retrieves the architecture by calling the getArchitecture function. -func (t *LocalTarget) GetArchitecture() (arch string, err error) { - if t.arch == "" { - t.arch, err = getArchitecture(t) - } - return t.arch, err -} - -func (t *RemoteTarget) GetArchitecture() (arch string, err error) { - if t.arch == "" { - t.arch, err = getArchitecture(t) - } - return t.arch, err -} - -func (t *LocalTarget) GetFamily() (family string, err error) { - if t.family == "" { - t.family, err = getFamily(t) - } - return t.family, err -} - -func (t *RemoteTarget) GetFamily() (family string, err error) { - if t.family == "" { - t.family, err = getFamily(t) - } - return t.family, err -} - -func (t *LocalTarget) GetModel() (family string, err error) { - if t.model == "" { - t.model, err = getModel(t) - } - return t.model, err -} - -func (t *RemoteTarget) GetModel() (family string, err error) { - if t.model == "" { - t.model, err = getModel(t) - } - return t.model, err -} - -func (t *LocalTarget) GetStepping() (stepping string, err error) { - if t.stepping == "" { - t.stepping, err = getStepping(t) - } - return t.stepping, err -} - -func (t *RemoteTarget) GetStepping() (stepping string, err error) { - if t.stepping == "" { - t.stepping, err = getStepping(t) - } - return t.stepping, err -} - -// GetVendor returns the vendor of the target. -// It retrieves the vendor by calling the GetVendor function. -func (t *LocalTarget) GetVendor() (arch string, err error) { - if t.vendor == "" { - t.vendor, err = getVendor(t) - } - return t.vendor, err -} - -func (t *RemoteTarget) GetVendor() (arch string, err error) { - if t.vendor == "" { - t.vendor, err = getVendor(t) - } - return t.vendor, err -} - -// CreateTempDirectory creates a temporary directory under the specified root directory. -// If the root directory is not specified, the temporary directory will be created in the current directory. -// It returns the path of the created temporary directory and any error encountered. -func (t *LocalTarget) CreateTempDirectory(rootDir string) (tempDir string, err error) { - if t.tempDir != "" { - return t.tempDir, nil - } - temp, err := os.MkdirTemp(rootDir, "perfspect.tmp.") - if err != nil { - return - } - tempDir, err = util.AbsPath(temp) - if err != nil { - return - } - t.tempDir = tempDir - return -} - -func (t *RemoteTarget) CreateTempDirectory(rootDir string) (tempDir string, err error) { - if t.tempDir != "" { - return t.tempDir, nil - } - var root string - if rootDir != "" { - root = fmt.Sprintf("--tmpdir=%s", rootDir) - } - cmd := exec.Command("mktemp", "-d", "-t", root, "perfspect.tmp.XXXXXXXXXX", "|", "xargs", "realpath") - tempDir, _, _, err = t.RunCommand(cmd, 0, true) - if err != nil { - return - } - tempDir = strings.TrimSpace(tempDir) - t.tempDir = tempDir - return -} - -// RemoveTempDirectory removes the temporary directory created by CreateTempDirectory. -func (t *LocalTarget) RemoveTempDirectory() (err error) { - if t.tempDir != "" { - err = t.RemoveDirectory(t.tempDir) - if err == nil { - t.tempDir = "" - } - } - return -} - -func (t *RemoteTarget) RemoveTempDirectory() (err error) { - if t.tempDir != "" { - err = t.RemoveDirectory(t.tempDir) - if err == nil { - t.tempDir = "" - } - } - return -} - -func (t *LocalTarget) GetTempDirectory() string { - return t.tempDir -} - -func (t *RemoteTarget) GetTempDirectory() string { - return t.tempDir -} - -// PushFile copies a file or directory from the source path to the destination path on the target. -// If the destination path is a directory, the file will be copied with the same name to that directory. -// If the destination path is a file, the file will be copied and overwritten. -// The file permissions of the source file will be preserved in the destination file. -func (t *LocalTarget) PushFile(srcPath string, dstPath string) (err error) { - srcFileStat, err := os.Stat(srcPath) - if err != nil { - return - } - if srcFileStat.IsDir() { - newDstDir := filepath.Join(dstPath, filepath.Base(srcPath)) - err = util.CreateDirectoryIfNotExists(newDstDir, 0755) - if err != nil { - return - } - err = util.CopyDirectory(srcPath, newDstDir) - return - } - err = util.CopyFile(srcPath, dstPath) - return -} - -func (t *RemoteTarget) PushFile(srcPath string, dstDir string) error { - stdout, stderr, exitCode, err := t.prepareAndRunSCPCommand(srcPath, dstDir, true) - slog.Debug("push file", slog.String("srcPath", srcPath), slog.String("dstDir", dstDir), slog.String("stdout", stdout), slog.String("stderr", stderr), slog.Int("exitCode", exitCode)) - return err -} - -// PullFile pulls a file from the target's source path to the destination directory. -// It is a convenience method that internally calls the PushFile method. -func (t *LocalTarget) PullFile(srcPath string, dstDir string) error { - return t.PushFile(srcPath, dstDir) -} - -func (t *RemoteTarget) PullFile(srcPath string, dstDir string) error { - stdout, stderr, exitCode, err := t.prepareAndRunSCPCommand(srcPath, dstDir, false) - slog.Debug("pull file", slog.String("srcPath", srcPath), slog.String("dstDir", dstDir), slog.String("stdout", stdout), slog.String("stderr", stderr), slog.Int("exitCode", exitCode)) - return err -} - -// CreateDirectory creates a new directory under the specified base directory. -// It returns the full path of the created directory and any error encountered. -func (t *LocalTarget) CreateDirectory(baseDir string, targetDir string) (dir string, err error) { - dir = filepath.Join(baseDir, targetDir) - err = os.Mkdir(dir, 0764) - return -} - -func (t *RemoteTarget) CreateDirectory(baseDir string, targetDir string) (dir string, err error) { - dir = filepath.Join(baseDir, targetDir) - cmd := exec.Command("mkdir", dir) - _, _, _, err = t.RunCommand(cmd, 0, true) - return -} - -// RemoveDirectory removes the specified target directory. -// If the target directory is not empty, it will be deleted along with all its contents. -// The method returns an error if any error occurs during the removal process. -func (t *LocalTarget) RemoveDirectory(targetDir string) (err error) { - if targetDir != "" { - err = os.RemoveAll(targetDir) - } - return -} - -func (t *RemoteTarget) RemoveDirectory(targetDir string) (err error) { - if targetDir != "" { - cmd := exec.Command("rm", "-rf", targetDir) - _, _, _, err = t.RunCommand(cmd, 0, true) - } - return -} - -// CanConnect checks if the local target can establish a connection. -func (t *LocalTarget) CanConnect() bool { - return true -} - -func (t *RemoteTarget) CanConnect() bool { - cmd := exec.Command("exit", "0") - _, _, _, err := t.RunCommand(cmd, 5, true) - return err == nil -} - -// CanElevatePrivileges (on LocalTarget) checks if the user is root or sudo can be used to elevate privileges. -// It returns true if the user is root or if the sudo password works. -// If the `sudo` command is configured, it will attempt to run a command with sudo -// and check if the password works. If the passwordless sudo is configured, -// it will also check if passwordless sudo works. -// Returns true if the user can elevate privileges, false otherwise. -func (t *LocalTarget) CanElevatePrivileges() bool { - if t.canElevate != 0 { - return t.canElevate == 1 - } - if t.IsSuperUser() { - t.canElevate = 1 - return true // user is root - } - if t.sudo != "" { - cmd := exec.Command("sudo", "-kS", "ls") - stdin, _ := cmd.StdinPipe() - go func() { - defer stdin.Close() - _, err := io.WriteString(stdin, t.sudo+"\n") - if err != nil { - slog.Error("error writing sudo password", slog.String("error", err.Error())) - } - }() - _, _, _, err := t.RunCommand(cmd, 0, true) - if err == nil { - t.canElevate = 1 - return true // sudo password works - } - } - cmd := exec.Command("sudo", "-kS", "ls") - _, _, _, err := t.RunCommand(cmd, 0, true) - if err == nil { // true - passwordless sudo works - t.canElevate = 1 - return true - } - t.canElevate = -1 - return false -} - -// CanElevatePrivileges (on RemoteTarget) checks if the user name is root or if sudo can be used to elevate privileges. -// Note that the sudo password is not used for this check. Password-less sudo is required. -func (t *RemoteTarget) CanElevatePrivileges() bool { - if t.canElevate != 0 { - return t.canElevate == 1 - } - if t.IsSuperUser() { - t.canElevate = 1 - return true - } - cmd := exec.Command("sudo", "-kS", "ls") - _, _, _, err := t.RunCommand(cmd, 0, true) - if err == nil { // true - passwordless sudo works - t.canElevate = 1 - return true - } - t.canElevate = -1 - return false -} - -// IsSuperUser checks if the current user is a superuser. -// It returns true if the user is a superuser, false otherwise. -func (t *LocalTarget) IsSuperUser() bool { - return os.Geteuid() == 0 -} - -func (t *RemoteTarget) IsSuperUser() bool { - return t.user == "root" -} - -// InstallLkms installs the specified LKMs (Loadable Kernel Modules) on the target. -// It returns the list of installed LKMs and any error encountered during the installation process. -func (t *LocalTarget) InstallLkms(lkms []string) (installedLkms []string, err error) { - return installLkms(t, lkms) -} - -func (t *RemoteTarget) InstallLkms(lkms []string) (installedLkms []string, err error) { - return installLkms(t, lkms) -} - -// UninstallLkms uninstalls the specified LKMs (Loadable Kernel Modules) from the target. -// It takes a slice of strings representing the names of the LKMs to be uninstalled. -// It returns an error if any error occurs during the uninstallation process. -func (t *LocalTarget) UninstallLkms(lkms []string) (err error) { - return uninstallLkms(t, lkms) -} - -func (t *RemoteTarget) UninstallLkms(lkms []string) (err error) { - return uninstallLkms(t, lkms) -} - -// GetName returns the name of the Target. -func (t *LocalTarget) GetName() (host string) { - return t.host -} - -func (t *RemoteTarget) GetName() (host string) { - if t.name == "" { - return t.host - } - return t.name -} - -// GetUserPath returns the user's PATH environment variable after verifying that it only contains valid paths. -// It checks each path in the PATH environment variable and filters out any non-path strings. -// The function returns the verified paths joined by ":" as a string. -func (t *LocalTarget) GetUserPath() (string, error) { - if t.userPath == "" { - // get user's PATH environment variable, verify that it only contains paths (mitigate risk raised by Checkmarx) - var verifiedPaths []string - pathEnv := os.Getenv("PATH") - pathEnvPaths := strings.SplitSeq(pathEnv, ":") - for p := range pathEnvPaths { - files, err := filepath.Glob(p) - // Goal is to filter out any non path strings - // Glob will throw an error on pattern mismatch and return no files if no files - if err == nil && len(files) > 0 { - verifiedPaths = append(verifiedPaths, p) - } - } - t.userPath = strings.Join(verifiedPaths, ":") - } - return t.userPath, nil -} - -func (t *RemoteTarget) GetUserPath() (string, error) { - if t.userPath == "" { - cmd := exec.Command("echo", "$PATH") - stdout, _, _, err := t.RunCommand(cmd, 0, true) - if err != nil { - return "", err - } - t.userPath = strings.TrimSpace(stdout) - } - return t.userPath, nil -} - -// helpers below - -func runLocalCommandWithInputWithTimeout(cmd *exec.Cmd, input string, timeout int) (stdout string, stderr string, exitCode int, err error) { - logInput := "" - if input != "" { - logInput = "******" - } - slog.Debug("running local command", slog.String("cmd", cmd.String()), slog.String("input", logInput), slog.Int("timeout", timeout)) - if timeout > 0 { - var cancel context.CancelFunc - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - commandWithContext := exec.CommandContext(ctx, cmd.Path, cmd.Args[1:]...) // nosemgrep - commandWithContext.Env = cmd.Env - cmd = commandWithContext - } - if input != "" { - cmd.Stdin = strings.NewReader(input) - } - var outbuf, errbuf strings.Builder - cmd.Stdout = &outbuf - cmd.Stderr = &errbuf - err = cmd.Run() - stdout = outbuf.String() - stderr = errbuf.String() - if err != nil { - exitError := &exec.ExitError{} - if errors.As(err, &exitError) { - exitCode = exitError.ExitCode() - } - } - return -} - -// TODO: does timeout make sense with async functions? -func runLocalCommandWithInputWithTimeoutAsync(cmd *exec.Cmd, stdoutChannel chan string, stderrChannel chan string, exitcodeChannel chan int, input string, timeout int) (err error) { - logInput := "" - if input != "" { - logInput = "******" - } - slog.Debug("running local command (async)", slog.String("cmd", cmd.String()), slog.String("input", logInput), slog.Int("timeout", timeout)) - if timeout > 0 { - var cancel context.CancelFunc - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - commandWithContext := exec.CommandContext(ctx, cmd.Path, cmd.Args[1:]...) // nosemgrep - commandWithContext.Env = cmd.Env - cmd = commandWithContext - } - if input != "" { - cmd.Stdin = strings.NewReader(input) - } - stdoutReader, err := cmd.StdoutPipe() - if err != nil { - err = fmt.Errorf("failed to get stdout pipe: %v", err) - return - } - stdoutScanner := bufio.NewScanner(stdoutReader) - stderrReader, err := cmd.StderrPipe() - if err != nil { - err = fmt.Errorf("failed to get stderr pipe: %v", err) - return - } - stderrScanner := bufio.NewScanner(stderrReader) - if err = cmd.Start(); err != nil { - err = fmt.Errorf("failed to run command (%s): %v", cmd, err) - return - } - go func() { - for stdoutScanner.Scan() { - text := stdoutScanner.Text() - stdoutChannel <- text - } - }() - go func() { - for stderrScanner.Scan() { - text := stderrScanner.Text() - stderrChannel <- text - } - }() - err = cmd.Wait() - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - exitcodeChannel <- exitError.ExitCode() - } else { - panic(fmt.Sprintf("err from cmd.Wait is not type exec.ExitError: %v", err)) - } - } else { - exitcodeChannel <- 0 - } - return nil -} - -func (t *RemoteTarget) prepareSSHFlags(scp bool, useControlMaster bool, prompt bool) (flags []string) { - flags = []string{ - "-2", - "-o", - "UserKnownHostsFile=/dev/null", - "-o", - "StrictHostKeyChecking=no", - "-o", - "ConnectTimeout=10", // This one exposes a bug in Windows' SSH client. Each connection takes - "-o", // 10 seconds to establish. https://github.com/PowerShell/Win32-OpenSSH/issues/1352 - "GSSAPIAuthentication=no", // This one is not supported, but is ignored on Windows. - "-o", - "ServerAliveInterval=30", - "-o", - "ServerAliveCountMax=10", // 30 * 10 = maximum 300 seconds before disconnect on no data - "-o", - "LogLevel=ERROR", - } - // turn on batch mode to avoid prompts for passwords - if !prompt { - promptFlags := []string{ - "-o", - "BatchMode=yes", - } - flags = append(flags, promptFlags...) - } - // when using a control master, a long-running remote program doesn't get terminated when the local ssh client is terminated - if useControlMaster { - controlPathFlags := []string{ - "-o", - "ControlPath=" + filepath.Join(os.TempDir(), fmt.Sprintf("control-%%h-%%p-%%r-%d", os.Getpid())), - "-o", - "ControlMaster=auto", - "-o", - "ControlPersist=1m", - } - flags = append(flags, controlPathFlags...) - } - if t.key != "" { - keyFlags := []string{ - "-o", - "PreferredAuthentications=publickey", - "-o", - "PasswordAuthentication=no", - "-i", - t.key, - } - flags = append(flags, keyFlags...) - } - if t.port != "" { - if scp { - flags = append(flags, "-P") - } else { - flags = append(flags, "-p") - } - flags = append(flags, t.port) - } - return -} - -func (t *RemoteTarget) prepareSSHCommand(command []string, useControlMaster bool, prompt bool) []string { - var cmd []string - cmd = append(cmd, "ssh") - cmd = append(cmd, t.prepareSSHFlags(false, useControlMaster, prompt)...) - if t.user != "" { - cmd = append(cmd, t.user+"@"+t.host) - } else { - cmd = append(cmd, t.host) - } - cmd = append(cmd, "--") - cmd = append(cmd, command...) - return cmd -} - -func (t *RemoteTarget) prepareSCPCommand(src string, dstDir string, push bool) []string { - var cmd []string - cmd = append(cmd, "scp") - cmd = append(cmd, t.prepareSSHFlags(true, true, false)...) - if push { - fileInfo, err := os.Stat(src) - if err != nil { - slog.Error("error getting file info", slog.String("src", src), slog.String("error", err.Error())) - return nil - } - if fileInfo.IsDir() { - cmd = append(cmd, "-r") - } - cmd = append(cmd, src) - dst := t.host + ":" + dstDir - if t.user != "" { - dst = t.user + "@" + dst - } - cmd = append(cmd, dst) - } else { // pull - s := t.host + ":" + src - if t.user != "" { - s = t.user + "@" + s - } - cmd = append(cmd, s) - cmd = append(cmd, dstDir) - } - return cmd -} - -func (t *RemoteTarget) prepareLocalCommand(cmd *exec.Cmd, useControlMaster bool) *exec.Cmd { - var name string - var args []string - usePass := t.key == "" && t.sshPass != "" - sshCommand := t.prepareSSHCommand(cmd.Args, useControlMaster, usePass) - if usePass { - name = t.sshpassPath - args = []string{"-e", "--"} - args = append(args, sshCommand...) - } else { - name = sshCommand[0] - args = sshCommand[1:] - } - localCommand := exec.Command(name, args...) // nosemgrep - if usePass { - localCommand.Env = append(localCommand.Env, "SSHPASS="+t.sshPass) - } - return localCommand -} - -func (t *RemoteTarget) prepareAndRunSCPCommand(srcPath string, dstDir string, isPush bool) (stdout string, stderr string, exitCode int, err error) { - scpCommand := t.prepareSCPCommand(srcPath, dstDir, isPush) - var name string - var args []string - usePass := t.key == "" && t.sshPass != "" - if usePass { - name = t.sshpassPath - args = append(args, "-e", "--") - args = append(args, scpCommand...) - } else { - name = scpCommand[0] - args = scpCommand[1:] - } - localCommand := exec.Command(name, args...) // nosemgrep - if usePass { - localCommand.Env = append(localCommand.Env, "SSHPASS="+t.sshPass) - } - stdout, stderr, exitCode, err = runLocalCommandWithInputWithTimeout(localCommand, "", 0) - return -} - -func getArchitecture(t Target) (arch string, err error) { - cmd := exec.Command("uname", "-m") - arch, _, _, err = t.RunCommand(cmd, 0, true) - if err != nil { - return - } - arch = strings.TrimSpace(arch) - return -} - -func getFamily(t Target) (family string, err error) { - cmd := exec.Command("bash", "-c", "lscpu | grep -i \"^CPU family:\" | awk '{print $NF}'") - family, _, _, err = t.RunCommand(cmd, 0, true) - if err != nil { - return - } - family = strings.TrimSpace(family) - return -} - -func getModel(t Target) (model string, err error) { - cmd := exec.Command("bash", "-c", "lscpu | grep -i model: | awk '{print $NF}'") - model, _, _, err = t.RunCommand(cmd, 0, true) - if err != nil { - return - } - model = strings.TrimSpace(model) - return -} - -func getStepping(t Target) (stepping string, err error) { - cmd := exec.Command("bash", "-c", "lscpu | grep -i stepping: | awk '{print $NF}'") - stepping, _, _, err = t.RunCommand(cmd, 0, true) - if err != nil { - return - } - stepping = strings.TrimSpace(stepping) - return -} - -func getVendor(t Target) (vendor string, err error) { - cmd := exec.Command("bash", "-c", "lscpu | grep -i \"^Vendor ID:\" | awk '{print $NF}'") - vendor, _, _, err = t.RunCommand(cmd, 0, true) - if err != nil { - return - } - vendor = strings.TrimSpace(vendor) - return -} - -func installLkms(t Target, lkms []string) (installedLkms []string, err error) { - if !t.CanElevatePrivileges() { - err = fmt.Errorf("can't elevate privileges; elevated privileges required to install lkms") - return - } - for _, lkm := range lkms { - slog.Debug("attempting to install kernel module", slog.String("lkm", lkm)) - _, _, _, err := t.RunCommand(exec.Command("modprobe", "--first-time", lkm), 10, true) - if err != nil { - slog.Debug("kernel module already installed or problem installing", slog.String("lkm", lkm), slog.String("error", err.Error())) - continue - } - slog.Debug("kernel module installed", slog.String("lkm", lkm)) - installedLkms = append(installedLkms, lkm) - } - return -} - -func uninstallLkms(t Target, lkms []string) (err error) { - if !t.CanElevatePrivileges() { - err = fmt.Errorf("can't elevate privileges; elevated privileges required to uninstall lkms") - return - } - for _, lkm := range lkms { - slog.Debug("attempting to uninstall kernel module", slog.String("lkm", lkm)) - _, _, _, err := t.RunCommand(exec.Command("modprobe", "-r", lkm), 10, true) - if err != nil { - slog.Error("error uninstalling kernel module", slog.String("lkm", lkm), slog.String("error", err.Error())) - continue - } - slog.Debug("kernel module uninstalled", slog.String("lkm", lkm)) - } - return -} - -// CanConnect checks if a connection can be established with the target. -// It returns true if a connection can be established, false otherwise. -func (t *RawTarget) CanConnect() bool { - panic("not implemented") // TODO: Implement -} - -// CanElevatePrivileges checks if the current user can elevate privileges. -// It returns true if the user can elevate privileges, false otherwise. -func (t *RawTarget) CanElevatePrivileges() bool { - panic("not implemented") // TODO: Implement -} - -// IsSuperUser checks if the current user is a superuser. -// It returns true if the user is a superuser, false otherwise. -func (t *RawTarget) IsSuperUser() bool { - panic("not implemented") // TODO: Implement -} - -// GetArchitecture returns the architecture of the target system. -// It returns a string representing the architecture and any error that occurred. -func (t *RawTarget) GetArchitecture() (arch string, err error) { - panic("not implemented") // TODO: Implement -} - -// GetFamily returns the family of the target system's CPU. -// It returns a string representing the family and any error that occurred. -func (t *RawTarget) GetFamily() (family string, err error) { - panic("not implemented") // TODO: Implement -} - -// GetModel returns the model of the target system's CPU. -// It returns a string representing the model and any error that occurred. -func (t *RawTarget) GetModel() (model string, err error) { - panic("not implemented") // TODO: Implement -} - -// GetStepping returns the stepping of the target system's CPU. -// It returns a string representing the stepping and any error that occurred. -func (t *RawTarget) GetStepping() (stepping string, err error) { - panic("not implemented") // TODO: Implement -} - -// GetVendor returns the vendor of the target system. -// It returns a string representing the vendor and any error that occurred. -func (t *RawTarget) GetVendor() (vendor string, err error) { - panic("not implemented") // TODO: Implement -} - -// GetName returns the name of the target system. -// It returns a string representing the host. -func (t *RawTarget) GetName() (name string) { - return t.name -} - -// GetUserPath returns the path of the current user on the target system. -// It returns a string representing the path and any error that occurred. -func (t *RawTarget) GetUserPath() (path string, err error) { - panic("not implemented") // TODO: Implement -} - -// RunCommand runs the specified command on the target. -// Arguments: -// - cmd: the command to run -// - timeout: the maximum time allowed for the command to run (zero means no timeout) -// - reuseSSHConnection: whether to reuse the SSH connection for the command (only relevant for RemoteTarget) -// It returns the standard output, standard error, exit code, and any error that occurred. -func (t *RawTarget) RunCommand(cmd *exec.Cmd, timeout int, reuseSSHConnection bool) (stdout string, stderr string, exitCode int, err error) { - panic("not implemented") // TODO: Implement -} - -// RunCommandAsync runs the specified command on the target in an asynchronous manner. -// Arguments: -// - cmd: the command to run -// - timeout: the maximum time allowed for the command to run (zero means no timeout) -// - reuseSSHConnection: whether to reuse the SSH connection for the command (only relevant for RemoteTarget) -// - stdoutChannel: a channel to send the standard output of the command -// - stderrChannel: a channel to send the standard error of the command -// - exitcodeChannel: a channel to send the exit code of the command -// - cmdChannel: a channel to send the command that was run -// It returns any error that occurred. -func (t *RawTarget) RunCommandAsync(cmd *exec.Cmd, timeout int, reuseSSHConnection bool, stdoutChannel chan string, stderrChannel chan string, exitcodeChannel chan int, cmdChannel chan *exec.Cmd) error { - panic("not implemented") // TODO: Implement -} - -// PushFile transfers a file from the local system to the target. -// It returns any error that occurred. -func (t *RawTarget) PushFile(srcPath string, dstPath string) error { - panic("not implemented") // TODO: Implement -} - -// PullFile transfers a file from the target to the local system. -// It returns any error that occurred. -func (t *RawTarget) PullFile(srcPath string, dstDir string) error { - panic("not implemented") // TODO: Implement -} - -// CreateDirectory creates a directory on the target at the specified path with the specified permissions. -// It returns the path of the created directory and any error that occurred. -func (t *RawTarget) CreateDirectory(baseDir string, targetDir string) (dir string, err error) { - panic("not implemented") // TODO: Implement -} - -// CreateTempDirectory creates a temporary directory on the target with the specified prefix. -// It returns the path of the created directory and any error that occurred. -func (t *RawTarget) CreateTempDirectory(rootDir string) (tempDir string, err error) { - panic("not implemented") // TODO: Implement -} - -// GetTempDirectory returns the path of the temporary directory on the target. It will be -// empty if the temporary directory has not been created yet. -func (t *RawTarget) GetTempDirectory() string { - panic("not implemented") // TODO: Implement -} - -// RemoveTempDirectory removes the temporary directory on the target. -// It returns any error that occurred. -func (t *RawTarget) RemoveTempDirectory() error { - panic("not implemented") // TODO: Implement -} - -// RemoveDirectory removes a directory from the target at the specified path. -// It returns any error that occurred. -func (t *RawTarget) RemoveDirectory(targetDir string) error { - panic("not implemented") // TODO: Implement -} - -// InstallLkms installs the specified Linux Kernel Modules (LKMs) on the target. -// It returns a list of installed LKMs and any error that occurred. -func (t *RawTarget) InstallLkms(lkms []string) (installedLkms []string, err error) { - panic("not implemented") // TODO: Implement -} - -// UninstallLkms uninstalls the specified Linux Kernel Modules (LKMs) from the target. -// It returns any error that occurred. -func (t *RawTarget) UninstallLkms(lkms []string) error { - panic("not implemented") // TODO: Implement -} diff --git a/internal/target/target_test.go b/internal/target/target_test.go index 0502d757..1eff9418 100644 --- a/internal/target/target_test.go +++ b/internal/target/target_test.go @@ -8,6 +8,7 @@ import ( ) func TestNew(t *testing.T) { + targets := []Target{} localTarget := NewLocalTarget() if localTarget == nil { t.Fatal("failed to create a local target") @@ -16,4 +17,11 @@ func TestNew(t *testing.T) { if remoteTarget == nil { t.Fatal("failed to create a remote target") } + targets = append(targets, localTarget) + targets = append(targets, remoteTarget) + for _, target := range targets { + if target.GetName() == "" { + t.Fatal("failed to get target name") + } + } }