2018-05-17 22:54:34 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2021-02-03 02:55:16 +00:00
|
|
|
"io/ioutil"
|
2018-05-17 22:54:34 +00:00
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/briandowns/spinner"
|
2021-03-13 00:42:38 +00:00
|
|
|
"github.com/urfave/cli/v2"
|
2018-05-17 22:54:34 +00:00
|
|
|
)
|
|
|
|
|
2021-03-13 00:42:38 +00:00
|
|
|
func queryCommand() *cli.Command {
|
2018-05-17 22:54:34 +00:00
|
|
|
var (
|
2020-01-21 06:16:11 +00:00
|
|
|
flHosts, flLabels, flQuery, flQueryName string
|
2021-02-03 02:55:16 +00:00
|
|
|
flQuiet, flExit, flPretty bool
|
2020-01-21 06:16:11 +00:00
|
|
|
flTimeout time.Duration
|
2018-05-17 22:54:34 +00:00
|
|
|
)
|
2021-03-13 00:42:38 +00:00
|
|
|
return &cli.Command{
|
2018-05-17 22:54:34 +00:00
|
|
|
Name: "query",
|
|
|
|
Usage: "Run a live query",
|
|
|
|
UsageText: `fleetctl query [options]`,
|
|
|
|
Flags: []cli.Flag{
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.StringFlag{
|
2018-05-17 22:54:34 +00:00
|
|
|
Name: "hosts",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"HOSTS"},
|
2018-05-17 22:54:34 +00:00
|
|
|
Value: "",
|
|
|
|
Destination: &flHosts,
|
|
|
|
Usage: "Comma separated hostnames to target",
|
|
|
|
},
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.StringFlag{
|
2018-05-17 22:54:34 +00:00
|
|
|
Name: "labels",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"LABELS"},
|
2018-05-17 22:54:34 +00:00
|
|
|
Value: "",
|
|
|
|
Destination: &flLabels,
|
|
|
|
Usage: "Comma separated label names to target",
|
|
|
|
},
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.BoolFlag{
|
2018-08-16 22:31:18 +00:00
|
|
|
Name: "quiet",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"QUIET"},
|
2018-08-16 22:31:18 +00:00
|
|
|
Destination: &flQuiet,
|
|
|
|
Usage: "Only print results (no status information)",
|
|
|
|
},
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.BoolFlag{
|
2018-08-16 22:31:18 +00:00
|
|
|
Name: "exit",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"EXIT"},
|
2018-08-16 22:31:18 +00:00
|
|
|
Destination: &flExit,
|
2019-01-15 19:06:22 +00:00
|
|
|
Usage: "Exit when 100% of online hosts have results returned",
|
2018-08-16 22:31:18 +00:00
|
|
|
},
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.StringFlag{
|
2018-05-17 22:54:34 +00:00
|
|
|
Name: "query",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"QUERY"},
|
2018-05-17 22:54:34 +00:00
|
|
|
Value: "",
|
|
|
|
Destination: &flQuery,
|
|
|
|
Usage: "Query to run",
|
|
|
|
},
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.StringFlag{
|
2020-01-21 06:16:11 +00:00
|
|
|
Name: "query-name",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"QUERYNAME"},
|
2020-01-21 06:16:11 +00:00
|
|
|
Value: "",
|
|
|
|
Destination: &flQueryName,
|
|
|
|
Usage: "Name of saved query to run",
|
|
|
|
},
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.BoolFlag{
|
2020-11-04 17:56:57 +00:00
|
|
|
Name: "pretty",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"PRETTY"},
|
2020-11-04 17:56:57 +00:00
|
|
|
Destination: &flPretty,
|
|
|
|
Usage: "Enable pretty-printing",
|
|
|
|
},
|
2021-03-13 00:42:38 +00:00
|
|
|
&cli.DurationFlag{
|
2019-01-15 19:06:22 +00:00
|
|
|
Name: "timeout",
|
2021-03-13 00:42:38 +00:00
|
|
|
EnvVars: []string{"TIMEOUT"},
|
2019-01-15 19:06:22 +00:00
|
|
|
Destination: &flTimeout,
|
|
|
|
Usage: "How long to run query before exiting (10s, 1h, etc.)",
|
|
|
|
},
|
2023-07-25 00:17:20 +00:00
|
|
|
&cli.UintFlag{
|
|
|
|
Name: teamFlagName,
|
|
|
|
Usage: "ID of the team where the named query belongs to (0 means global)",
|
|
|
|
},
|
2021-02-03 02:55:16 +00:00
|
|
|
configFlag(),
|
|
|
|
contextFlag(),
|
|
|
|
debugFlag(),
|
2018-05-17 22:54:34 +00:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
fleet, err := clientFromCLI(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if flHosts == "" && flLabels == "" {
|
2022-04-20 17:35:46 +00:00
|
|
|
return errors.New("No hosts or labels targeted. Please provide either --hosts or --labels.")
|
2018-05-17 22:54:34 +00:00
|
|
|
}
|
|
|
|
|
2020-01-21 06:16:11 +00:00
|
|
|
if flQuery != "" && flQueryName != "" {
|
2021-11-24 20:56:54 +00:00
|
|
|
return errors.New("--query and --query-name must not be provided together")
|
2020-01-21 06:16:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if flQueryName != "" {
|
2023-07-25 00:17:20 +00:00
|
|
|
var teamID *uint
|
|
|
|
if tid := c.Uint(teamFlagName); tid != 0 {
|
|
|
|
teamID = &tid
|
|
|
|
}
|
|
|
|
q, err := fleet.GetQuerySpec(teamID, flQueryName)
|
2020-01-21 06:16:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Query '%s' not found", flQueryName)
|
|
|
|
}
|
|
|
|
flQuery = q.Query
|
|
|
|
}
|
|
|
|
|
2018-05-17 22:54:34 +00:00
|
|
|
if flQuery == "" {
|
2021-11-24 20:56:54 +00:00
|
|
|
return errors.New("Query must be specified with --query or --query-name")
|
2018-05-17 22:54:34 +00:00
|
|
|
}
|
|
|
|
|
2020-11-04 17:56:57 +00:00
|
|
|
var output outputWriter
|
|
|
|
if flPretty {
|
|
|
|
output = newPrettyWriter()
|
|
|
|
} else {
|
2021-09-10 19:26:39 +00:00
|
|
|
output = newJsonWriter(c.App.Writer)
|
2020-11-04 17:56:57 +00:00
|
|
|
}
|
|
|
|
|
2018-05-17 22:54:34 +00:00
|
|
|
hosts := strings.Split(flHosts, ",")
|
|
|
|
labels := strings.Split(flLabels, ",")
|
|
|
|
|
|
|
|
res, err := fleet.LiveQuery(flQuery, labels, hosts)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tick := time.NewTicker(100 * time.Millisecond)
|
|
|
|
defer tick.Stop()
|
|
|
|
|
|
|
|
// See charsets at
|
|
|
|
// https://godoc.org/github.com/briandowns/spinner#pkg-variables
|
|
|
|
s := spinner.New(spinner.CharSets[24], 200*time.Millisecond)
|
|
|
|
s.Writer = os.Stderr
|
2020-11-03 23:50:36 +00:00
|
|
|
if flQuiet {
|
|
|
|
s.Writer = ioutil.Discard
|
2018-08-16 22:31:18 +00:00
|
|
|
}
|
2020-11-03 23:50:36 +00:00
|
|
|
s.Start()
|
2018-05-17 22:54:34 +00:00
|
|
|
|
2019-01-15 19:06:22 +00:00
|
|
|
var timeoutChan <-chan time.Time
|
|
|
|
if flTimeout > 0 {
|
|
|
|
timeoutChan = time.After(flTimeout)
|
|
|
|
} else {
|
2019-03-10 20:51:11 +00:00
|
|
|
// Channel that never fires (so that we can
|
|
|
|
// read from the channel in the below select
|
|
|
|
// statement without panicking)
|
2019-01-15 19:06:22 +00:00
|
|
|
timeoutChan = make(chan time.Time)
|
|
|
|
}
|
|
|
|
|
2018-05-17 22:54:34 +00:00
|
|
|
for {
|
|
|
|
select {
|
2019-01-15 19:06:22 +00:00
|
|
|
// Print a result
|
2018-05-17 22:54:34 +00:00
|
|
|
case hostResult := <-res.Results():
|
2018-09-07 22:37:10 +00:00
|
|
|
s.Stop()
|
2020-11-04 17:56:57 +00:00
|
|
|
|
|
|
|
if err := output.WriteResult(hostResult); err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "Error writing result: %s\n", err)
|
2018-05-17 22:54:34 +00:00
|
|
|
}
|
2020-11-04 17:56:57 +00:00
|
|
|
|
2018-09-07 22:37:10 +00:00
|
|
|
s.Start()
|
2018-05-17 22:54:34 +00:00
|
|
|
|
2019-01-15 19:06:22 +00:00
|
|
|
// Print an error
|
2018-05-17 22:54:34 +00:00
|
|
|
case err := <-res.Errors():
|
|
|
|
fmt.Fprintf(os.Stderr, "Error talking to server: %s\n", err.Error())
|
|
|
|
|
2019-01-15 19:06:22 +00:00
|
|
|
// Update status message on interval
|
2018-05-17 22:54:34 +00:00
|
|
|
case <-tick.C:
|
|
|
|
status := res.Status()
|
|
|
|
totals := res.Totals()
|
|
|
|
var percentTotal, percentOnline float64
|
|
|
|
var responded, total, online uint
|
|
|
|
if status != nil && totals != nil {
|
|
|
|
total = totals.Total
|
|
|
|
online = totals.Online
|
|
|
|
responded = status.ActualResults
|
|
|
|
if total > 0 {
|
|
|
|
percentTotal = 100 * float64(responded) / float64(total)
|
|
|
|
}
|
|
|
|
if online > 0 {
|
|
|
|
percentOnline = 100 * float64(responded) / float64(online)
|
|
|
|
}
|
|
|
|
}
|
2018-08-16 22:31:18 +00:00
|
|
|
|
2018-09-07 22:37:10 +00:00
|
|
|
msg := fmt.Sprintf(" %.f%% responded (%.f%% online) | %d/%d targeted hosts (%d/%d online)", percentTotal, percentOnline, responded, total, responded, online)
|
2021-11-01 18:31:01 +00:00
|
|
|
|
|
|
|
s.Lock()
|
2020-11-03 23:50:36 +00:00
|
|
|
s.Suffix = msg
|
2021-11-01 18:31:01 +00:00
|
|
|
s.Unlock()
|
|
|
|
|
2020-01-17 01:45:26 +00:00
|
|
|
if total == responded && status != nil {
|
2018-09-07 22:37:10 +00:00
|
|
|
s.Stop()
|
|
|
|
if !flQuiet {
|
2019-01-15 19:06:22 +00:00
|
|
|
fmt.Fprintln(os.Stderr, msg)
|
2018-09-07 22:37:10 +00:00
|
|
|
}
|
|
|
|
return nil
|
2018-08-16 22:31:18 +00:00
|
|
|
}
|
2019-01-15 19:06:22 +00:00
|
|
|
|
Fix small `fleetctl query` bug when running with `--exit` flag (#11894)
Bug found while working on #10957.
Can be reproduced in our dogfood environment:
```
fleetctl query --context dogfood --query "SELECT * from osquery_info;" --hosts dogfood-centos-box --exit
⠋ %
```
With the changes in this PR:
```
fleetctl query --context dogfood --query "SELECT * from osquery_info;" --hosts dogfood-centos-box --exit
{"host":"dogfood-centos-box","rows":[{"build_distro":"centos7","build_platform":"linux","config_hash":"e3832343af2f8dc3e5ab62e709c78d3c3ef32b86","config_valid":"1","extensions":"active","host_display_name":"dogfood-centos-box","host_hostname":"dogfood-centos-box","instance_id":"9f0f6433-fbcf-4f15-8f1b-4dedc669ee2d","pid":"2760450","platform_mask":"9","start_time":"1684821735","uuid":"911CBDBA-7B3A-4B96-88F7-B28CECBEF400","version":"5.8.2","watcher":"2760447"}]}
⠦ 0% responded (0% online) | 0/1 targeted hosts (0/1 online) %
```
2023-05-25 11:12:45 +00:00
|
|
|
if status != nil && totals != nil && responded >= online && flExit {
|
|
|
|
s.Stop()
|
|
|
|
if !flQuiet {
|
|
|
|
fmt.Fprintln(os.Stderr, msg)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-01-15 19:06:22 +00:00
|
|
|
// Check for timeout expiring
|
|
|
|
case <-timeoutChan:
|
|
|
|
s.Stop()
|
|
|
|
if !flQuiet {
|
|
|
|
fmt.Fprintln(os.Stderr, s.Suffix+"\nStopped by timeout")
|
|
|
|
}
|
|
|
|
return nil
|
2018-05-17 22:54:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|