Support command status for commands and host for results in fleetctl (#37133)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #36870 # Checklist for submitter If some of the following don't apply, delete the relevant line. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
This commit is contained in:
@@ -71,3 +71,10 @@ func byMDMCommandRequestType() cli.Flag {
|
||||
Usage: "Filter MDM commands by type.",
|
||||
}
|
||||
}
|
||||
|
||||
func withMDMCommandStatusFilter() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: "command_status",
|
||||
Usage: "Filter MDM commands by command status in a comma-separated list. Valid values are 'pending', 'ran', and 'failed'. ",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1473,6 +1473,7 @@ func getMDMCommandResultsCommand() *cli.Command {
|
||||
Usage: "Filter MDM commands by ID.",
|
||||
Required: true,
|
||||
},
|
||||
byHostIdentifier(),
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
client, err := clientFromCLI(c)
|
||||
@@ -1485,10 +1486,10 @@ func getMDMCommandResultsCommand() *cli.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := client.MDMGetCommandResults(c.String("id"))
|
||||
res, err := client.MDMGetCommandResults(c.String("id"), c.String("host"))
|
||||
if err != nil {
|
||||
var nfe service.NotFoundErr
|
||||
if errors.As(err, &nfe) {
|
||||
if errors.As(err, &nfe) && c.String("host") == "" {
|
||||
return errors.New("The command doesn't exist. Please provide a valid command ID. To see a list of commands that were run, run `fleetctl get mdm-commands`.")
|
||||
}
|
||||
|
||||
@@ -1569,6 +1570,7 @@ func getMDMCommandsCommand() *cli.Command {
|
||||
debugFlag(),
|
||||
byHostIdentifier(),
|
||||
byMDMCommandRequestType(),
|
||||
withMDMCommandStatusFilter(),
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
client, err := clientFromCLI(c)
|
||||
@@ -1581,10 +1583,18 @@ func getMDMCommandsCommand() *cli.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
commandStatuses := []fleet.MDMCommandStatusFilter{}
|
||||
if c.IsSet("command_status") {
|
||||
for val := range strings.SplitSeq(c.String("command_status"), ",") {
|
||||
commandStatuses = append(commandStatuses, fleet.MDMCommandStatusFilter(val))
|
||||
}
|
||||
}
|
||||
|
||||
opts := fleet.MDMCommandListOptions{
|
||||
Filters: fleet.MDMCommandFilters{
|
||||
HostIdentifier: c.String("host"),
|
||||
RequestType: c.String("type"),
|
||||
HostIdentifier: c.String("host"),
|
||||
RequestType: c.String("type"),
|
||||
CommandStatuses: commandStatuses,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1595,7 +1605,13 @@ func getMDMCommandsCommand() *cli.Command {
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(results) == 0 && opts.Filters.HostIdentifier == "" && opts.Filters.RequestType == "" {
|
||||
|
||||
if len(results) == 0 {
|
||||
if opts.Filters.HostIdentifier != "" {
|
||||
log(c, "No MDM commands have been run on this host.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
log(c, "You haven't run any MDM commands. Run MDM commands with the `fleetctl mdm run-command` command.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2593,6 +2593,18 @@ func TestGetMDMCommandResults(t *testing.T) {
|
||||
{ID: 2, UUID: uuids[1], Hostname: "host2"},
|
||||
}, nil
|
||||
}
|
||||
ds.GetHostMDMIdentifiersFunc = func(ctx context.Context, identifer string, teamFilter fleet.TeamFilter) ([]*fleet.HostMDMIdentifiers, error) {
|
||||
return []*fleet.HostMDMIdentifiers{
|
||||
{
|
||||
UUID: "device1",
|
||||
HardwareSerial: "C02XXXXXXX1",
|
||||
Hostname: "host1",
|
||||
ID: 1,
|
||||
TeamID: ptr.Uint(1),
|
||||
Platform: "darwin",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
ds.GetMDMAppleCommandResultsFunc = func(ctx context.Context, commandUUID string, hostUUID string) ([]*fleet.MDMCommandResult, error) {
|
||||
switch commandUUID {
|
||||
case "empty-cmd":
|
||||
@@ -3000,6 +3012,66 @@ RESULTS:
|
||||
ds.GetMDMWindowsCommandResultsFuncInvoked = false
|
||||
require.False(t, ds.GetMDMAppleCommandResultsFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("host specific results", func(t *testing.T) {
|
||||
expectedOutput := strings.TrimSpace(`
|
||||
ID:
|
||||
valid-cmd
|
||||
|
||||
TIME:
|
||||
2023-04-04T15:29:00Z
|
||||
|
||||
TYPE:
|
||||
test
|
||||
|
||||
STATUS:
|
||||
Acknowledged
|
||||
|
||||
HOSTNAME:
|
||||
host1
|
||||
|
||||
PAYLOAD:
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Command</key>
|
||||
<dict>
|
||||
<key>ManagedOnly</key>
|
||||
<false/>
|
||||
<key>RequestType</key>
|
||||
<string>ProfileList</string>
|
||||
</dict>
|
||||
<key>CommandUUID</key>
|
||||
<string>0001_ProfileList</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
|
||||
RESULTS:
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CommandUUID</key>
|
||||
<string>6d7cb698-8d93-45a3-b544-71aef37d42e8</string>
|
||||
<key>Status</key>
|
||||
<string>Acknowledged</string>
|
||||
<key>UDID</key>
|
||||
<string>419D46EC-06E6-557C-AD52-601BA0667730</string>
|
||||
</dict>
|
||||
</plist>`)
|
||||
|
||||
platform = "darwin"
|
||||
buf, err := RunAppNoChecks([]string{"get", "mdm-command-results", "--id", "valid-cmd", "--host", "device1"})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, buf.String(), expectedOutput)
|
||||
require.True(t, ds.GetMDMCommandPlatformFuncInvoked)
|
||||
ds.GetMDMCommandPlatformFuncInvoked = false
|
||||
require.False(t, ds.GetMDMWindowsCommandResultsFuncInvoked)
|
||||
require.True(t, ds.GetMDMAppleCommandResultsFuncInvoked)
|
||||
ds.GetMDMAppleCommandResultsFuncInvoked = false
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMDMCommands(t *testing.T) {
|
||||
@@ -3008,6 +3080,14 @@ func TestGetMDMCommands(t *testing.T) {
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
|
||||
}
|
||||
ds.HostLiteByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.HostLite, error) {
|
||||
fmt.Println("Called", identifier)
|
||||
if identifier == "foo" || identifier == "h1" {
|
||||
return &fleet.HostLite{ID: 1, UUID: "h1", Hostname: "host1"}, nil
|
||||
}
|
||||
return nil, errors.New(fleet.HostIdentiferNotFound)
|
||||
}
|
||||
|
||||
var empty bool
|
||||
var listErr error
|
||||
var noHostErr error
|
||||
@@ -3029,7 +3109,7 @@ func TestGetMDMCommands(t *testing.T) {
|
||||
if expectRequestType {
|
||||
require.NotEmpty(t, listOpts.Filters.RequestType)
|
||||
}
|
||||
|
||||
fmt.Println("Returning commands")
|
||||
return []*fleet.MDMCommand{
|
||||
{
|
||||
HostUUID: "h1",
|
||||
@@ -3123,6 +3203,20 @@ The list of 3 most recent commands:
|
||||
_, err = RunAppNoChecks([]string{"get", "mdm-commands", "--host", "foo"})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fleet.HostIdentiferNotFound)
|
||||
|
||||
// Empty results when using host identifier
|
||||
listErr = nil
|
||||
empty = true
|
||||
expectIdentifier = true
|
||||
noHostErr = nil
|
||||
buf, err = RunAppNoChecks([]string{"get", "mdm-commands", "--host", "foo"})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, buf.String(), "No MDM commands have been run on this host.")
|
||||
|
||||
// Command status no host
|
||||
_, err = RunAppNoChecks([]string{"get", "mdm-commands", "--command_status", "ran"})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, `"host_identifier" must be specified when filtering by "command_status"`)
|
||||
}
|
||||
|
||||
func TestUserIsObserver(t *testing.T) {
|
||||
|
||||
@@ -283,7 +283,7 @@ func (c *Client) deleteMacOSSetupAssistant(teamID *uint) error {
|
||||
func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDMCommand, error) {
|
||||
const defaultCommandsPerPage = 20
|
||||
|
||||
verb, path := http.MethodGet, "/api/latest/fleet/mdm/commands"
|
||||
verb, path := http.MethodGet, "/api/latest/fleet/commands"
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("per_page", fmt.Sprint(defaultCommandsPerPage))
|
||||
@@ -292,6 +292,15 @@ func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDM
|
||||
query.Set("host_identifier", opts.Filters.HostIdentifier)
|
||||
query.Set("request_type", opts.Filters.RequestType)
|
||||
|
||||
var statuses []string
|
||||
if len(opts.Filters.CommandStatuses) > 0 {
|
||||
statuses = make([]string, 0, len(opts.Filters.CommandStatuses))
|
||||
for _, s := range opts.Filters.CommandStatuses {
|
||||
statuses = append(statuses, string(s))
|
||||
}
|
||||
}
|
||||
query.Set("command_status", strings.Join(statuses, ","))
|
||||
|
||||
var responseBody listMDMCommandsResponse
|
||||
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode())
|
||||
if err != nil {
|
||||
@@ -301,11 +310,12 @@ func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDM
|
||||
return responseBody.Results, nil
|
||||
}
|
||||
|
||||
func (c *Client) MDMGetCommandResults(commandUUID string) ([]*fleet.MDMCommandResult, error) {
|
||||
verb, path := http.MethodGet, "/api/latest/fleet/mdm/commandresults"
|
||||
func (c *Client) MDMGetCommandResults(commandUUID, hostIdentifier string) ([]*fleet.MDMCommandResult, error) {
|
||||
verb, path := http.MethodGet, "/api/latest/fleet/commands/results"
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("command_uuid", commandUUID)
|
||||
query.Set("host_identifier", hostIdentifier)
|
||||
|
||||
var responseBody getMDMCommandResultsResponse
|
||||
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode())
|
||||
|
||||
@@ -1060,7 +1060,6 @@ func (svc *Service) ListMDMCommands(ctx context.Context, opts *fleet.MDMCommandL
|
||||
return nil, nil, fleet.NewInvalidArgumentError("Invalid Host", fleet.HostIdentiferNotFound).WithStatus(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
return results, total, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user