fleet/cmd/fleetctl/scripts.go
Jahziel Villasana-Espinoza 205338bfa3
feat: update error message for script timeouts (#17215)
> Related issue: #16019

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Manual QA for all new/changed functionality
2024-02-27 16:19:34 -05:00

163 lines
3.7 KiB
Go

package main
import (
"errors"
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"unicode/utf8"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/urfave/cli/v2"
)
func runScriptCommand() *cli.Command {
return &cli.Command{
Name: "run-script",
Aliases: []string{"run_script"},
Usage: `Run a live script on one host and get results back.`,
UsageText: `fleetctl run-script [options]`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "script-path",
Usage: "The path to the script.",
Required: true,
},
&cli.StringFlag{
Name: "host",
Usage: "A host, specified by hostname, serial number, UUID, osquery host ID, or node key.",
Required: true,
},
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
appCfg, err := client.GetAppConfig()
if err != nil {
return err
}
if appCfg.ServerSettings.ScriptsDisabled {
return errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg)
}
path := c.String("script-path")
if err := validateScriptPath(path); err != nil {
return err
}
ident := c.String("host")
h, err := client.HostByIdentifier(ident)
if err != nil {
var nfe service.NotFoundErr
if errors.As(err, &nfe) {
return errors.New(fleet.RunScriptHostNotFoundErrMsg)
}
var sce fleet.ErrWithStatusCode
if errors.As(err, &sce) {
if sce.StatusCode() == http.StatusForbidden {
return errors.New(fleet.RunScriptForbiddenErrMsg)
}
}
return err
}
if h.Status != fleet.StatusOnline {
return errors.New(fleet.RunScriptHostOfflineErrMsg)
}
b, err := os.ReadFile(path)
if err != nil {
return err
}
if err := fleet.ValidateHostScriptContents(string(b)); err != nil {
return err
}
fmt.Println("\nScript is running. Please wait for it to finish...")
res, err := client.RunHostScriptSync(h.ID, b)
if err != nil {
return err
}
if err := renderScriptResult(c, res); err != nil {
return err
}
return nil
},
}
}
func renderScriptResult(c *cli.Context, res *fleet.HostScriptResult) error {
tmpl := template.Must(template.New("").Parse(`
{{ if .ErrorMsg -}}
Error: {{ .ErrorMsg }}
{{- else -}}
Exit code: {{ .ExitCode }} ({{ .ExitMessage }})
{{- end }}
{{ if .ShowOutput }}
Output {{- if .ExecTimeout }} before timeout {{- end }}:
-------------------------------------------------------------------------------------
{{ .Output }}
-------------------------------------------------------------------------------------
{{- end }}
`))
data := struct {
ExecTimeout bool
ErrorMsg string
ExitCode *int64
ExitMessage string
Output string
ShowOutput bool
}{
ExitCode: res.ExitCode,
ExitMessage: "Script failed.",
ShowOutput: true,
}
switch {
case res.ExitCode == nil:
data.ErrorMsg = res.Message
case *res.ExitCode == -2:
data.ShowOutput = false
data.ErrorMsg = res.Message
case *res.ExitCode == -1:
data.ExecTimeout = true
data.ErrorMsg = res.Message
case *res.ExitCode == 0:
data.ExitMessage = "Script ran successfully."
}
if len(res.Output) >= fleet.MaxScriptRuneLen && utf8.RuneCountInString(res.Output) >= fleet.MaxScriptRuneLen {
data.Output = "Fleet records the last 10,000 characters to prevent downtime.\n\n" + res.Output
} else {
data.Output = res.Output
}
return tmpl.Execute(c.App.Writer, data)
}
func validateScriptPath(path string) error {
extension := filepath.Ext(path)
if extension == ".sh" || extension == ".ps1" {
return nil
}
return errors.New(fleet.RunScriptInvalidTypeErrMsg)
}