Add "batch script host results" API (#32174)
for #31536 # Details This PR adds a new API as specced in [the API PR](https://github.com/fleetdm/fleet/blob/9bf150580ba75fdfb6f5273e59cc14de45c7813d/docs/REST%20API/rest-api.md#list-hosts-targeted-in-batch-script) for scheduled scripts. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [X] QA'd all new/changed functionality manually ran a batch script on 100 hosts and ran the API in Postman for each status, then canceled the batch and ran the API to check the canceled status. --------- Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
This commit is contained in:
co-authored by
Lucas Manuel Rodriguez
parent
399355bfad
commit
a87498421b
@@ -0,0 +1 @@
|
||||
- Added "list hosts targeted in batch script" API
|
||||
+107
-23
@@ -1071,6 +1071,82 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListBatchScriptHosts(ctx context.Context, batchScriptExecutionID string, batchScriptExecutionStatus fleet.BatchScriptExecutionStatus, opt fleet.ListOptions) (hosts []fleet.BatchScriptHost, meta *fleet.PaginationMetadata, count uint, err error) {
|
||||
countStmt := `
|
||||
SELECT COUNT(*)
|
||||
FROM
|
||||
hosts h
|
||||
%s
|
||||
WHERE
|
||||
%s
|
||||
`
|
||||
|
||||
sqlStmt := `
|
||||
SELECT
|
||||
h.id,
|
||||
h.hostname as display_name,
|
||||
? as status,
|
||||
-- pending hosts will have "updated_at" set in the db, but since
|
||||
-- we're using it to mean "executed at" we'll return it as empty.
|
||||
CASE
|
||||
WHEN ? != 'pending' THEN hsr.updated_at
|
||||
ELSE NULL
|
||||
END as updated_at,
|
||||
COALESCE(LEFT(hsr.output, 100), '') as output,
|
||||
COALESCE(hsr.execution_id, '') as execution_id
|
||||
FROM
|
||||
hosts h
|
||||
%s
|
||||
WHERE
|
||||
%s
|
||||
`
|
||||
|
||||
// Validate the batch status.
|
||||
if !batchScriptExecutionStatus.IsValid() {
|
||||
return nil, nil, 0, errors.New("invalid batch execution status")
|
||||
}
|
||||
|
||||
queryParams := []interface{}{}
|
||||
batchScriptExecutionJoin, batchScriptExecutionFilter, queryParams := ds.getBatchExecutionFilters(queryParams, fleet.HostListOptions{
|
||||
BatchScriptExecutionIDFilter: &batchScriptExecutionID,
|
||||
BatchScriptExecutionStatusFilter: batchScriptExecutionStatus,
|
||||
})
|
||||
|
||||
// Count the total number of hosts matching the filters.
|
||||
// Do this before we add more "where" params for the main query.
|
||||
countStmt = fmt.Sprintf(countStmt, batchScriptExecutionJoin, batchScriptExecutionFilter)
|
||||
dbReader := ds.reader(ctx)
|
||||
if err = sqlx.GetContext(ctx, dbReader, &count, countStmt, queryParams...); err != nil {
|
||||
return nil, nil, 0, ctxerr.Wrap(ctx, err, "list batch scripts count")
|
||||
}
|
||||
|
||||
// Add in the params for the main query SELECT.
|
||||
queryParams = append([]interface{}{batchScriptExecutionStatus, batchScriptExecutionStatus}, queryParams...) // make a copy so we don't modify the original slice
|
||||
// Add in the paging params.
|
||||
sqlStmt, queryParams = appendListOptionsWithCursorToSQL(sqlStmt, queryParams, &opt)
|
||||
|
||||
// Run the main query to get the list of hosts.
|
||||
sqlStmt = fmt.Sprintf(sqlStmt, batchScriptExecutionJoin, batchScriptExecutionFilter)
|
||||
if err = sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, sqlStmt, queryParams...); err != nil {
|
||||
return nil, nil, 0, ctxerr.Wrap(ctx, err, "list batch script hosts")
|
||||
}
|
||||
|
||||
if opt.IncludeMetadata {
|
||||
meta = &fleet.PaginationMetadata{
|
||||
HasPreviousResults: opt.Page > 0,
|
||||
TotalResults: count,
|
||||
}
|
||||
// `appendListOptionsWithCursorToSQL` used above to build the query statement will cause this discrepancy.
|
||||
// This is intentional so that we can check whether we have another page.
|
||||
if len(hosts) > int(opt.PerPage) { //nolint:gosec // dismiss G115
|
||||
meta.HasNextResults = true
|
||||
hosts = hosts[:len(hosts)-1]
|
||||
}
|
||||
}
|
||||
|
||||
return hosts, meta, count, nil
|
||||
}
|
||||
|
||||
// TODO(Sarah): Do we need to reconcile mutually exclusive filters?
|
||||
func (ds *Datastore) applyHostFilters(
|
||||
ctx context.Context, opt fleet.HostListOptions, sqlStmt string, filter fleet.TeamFilter, selectParams []interface{},
|
||||
@@ -1207,29 +1283,9 @@ func (ds *Datastore) applyHostFilters(
|
||||
// BatchScriptExecutionIDFilter is set. This allows us to filter hosts based on the status of
|
||||
// batch script executions.
|
||||
batchScriptExecutionJoin := ""
|
||||
batchScriptExecutionIDFilter := "TRUE"
|
||||
batchScriptExecutionFilter := "TRUE"
|
||||
if opt.BatchScriptExecutionIDFilter != nil {
|
||||
batchScriptExecutionJoin = `LEFT JOIN batch_activity_host_results bsehr ON h.id = bsehr.host_id`
|
||||
batchScriptExecutionIDFilter = `bsehr.batch_execution_id = ?`
|
||||
whereParams = append(whereParams, *opt.BatchScriptExecutionIDFilter)
|
||||
if opt.BatchScriptExecutionStatusFilter.IsValid() {
|
||||
batchScriptExecutionJoin += ` LEFT JOIN host_script_results hsr ON bsehr.host_execution_id = hsr.execution_id`
|
||||
switch opt.BatchScriptExecutionStatusFilter {
|
||||
case fleet.BatchScriptExecutionRan:
|
||||
batchScriptExecutionIDFilter += ` AND hsr.exit_code = 0`
|
||||
case fleet.BatchScriptExecutionPending:
|
||||
// Pending can mean "waiting for execution" or "waiting for results".
|
||||
batchScriptExecutionJoin += ` LEFT JOIN upcoming_activities ua ON ua.execution_id = bsehr.host_execution_id`
|
||||
batchScriptExecutionIDFilter += ` AND ((ua.execution_id IS NOT NULL) OR (hsr.host_id is NOT NULL AND hsr.exit_code IS NULL AND hsr.canceled = 0 AND bsehr.error IS NULL))`
|
||||
case fleet.BatchScriptExecutionErrored:
|
||||
// TODO - remove exit code condition when we split up "errored" and "failed"
|
||||
batchScriptExecutionIDFilter += ` AND hsr.exit_code > 0`
|
||||
case fleet.BatchScriptExecutionIncompatible:
|
||||
batchScriptExecutionIDFilter += ` AND bsehr.error IS NOT NULL`
|
||||
case fleet.BatchScriptExecutionCanceled:
|
||||
batchScriptExecutionIDFilter += ` AND hsr.exit_code IS NULL AND hsr.canceled = 1`
|
||||
}
|
||||
}
|
||||
batchScriptExecutionJoin, batchScriptExecutionFilter, whereParams = ds.getBatchExecutionFilters(whereParams, opt)
|
||||
}
|
||||
|
||||
sqlStmt += fmt.Sprintf(
|
||||
@@ -1272,7 +1328,7 @@ func (ds *Datastore) applyHostFilters(
|
||||
softwareFilter,
|
||||
munkiFilter,
|
||||
lowDiskSpaceFilter,
|
||||
batchScriptExecutionIDFilter,
|
||||
batchScriptExecutionFilter,
|
||||
)
|
||||
|
||||
now := ds.clock.Now()
|
||||
@@ -1319,6 +1375,34 @@ func (ds *Datastore) applyHostFilters(
|
||||
return sqlStmt, params, nil
|
||||
}
|
||||
|
||||
func (*Datastore) getBatchExecutionFilters(whereParams []interface{}, opt fleet.HostListOptions) (batchScriptExecutionJoin string, batchScriptExecutionFilter string, updatedWhereParams []interface{}) {
|
||||
batchScriptExecutionJoin = `LEFT JOIN batch_activity_host_results bsehr ON h.id = bsehr.host_id`
|
||||
batchScriptExecutionFilter = `bsehr.batch_execution_id = ?`
|
||||
whereParams = append(whereParams, *opt.BatchScriptExecutionIDFilter)
|
||||
if opt.BatchScriptExecutionStatusFilter.IsValid() {
|
||||
batchScriptExecutionJoin += ` LEFT JOIN host_script_results hsr ON bsehr.host_execution_id = hsr.execution_id`
|
||||
switch opt.BatchScriptExecutionStatusFilter {
|
||||
case fleet.BatchScriptExecutionRan:
|
||||
batchScriptExecutionFilter += ` AND hsr.exit_code = 0 AND hsr.canceled = 0`
|
||||
case fleet.BatchScriptExecutionPending:
|
||||
// Pending can mean "waiting for execution" or "waiting for results".
|
||||
// hsr.exit_code IS NULL <- this means the script has not reported back
|
||||
// (hsr.canceled IS NULL OR hsr.canceled = 0) <- this can mean the script is running, or that it hasn't been activated yet,
|
||||
// but either way we haven't canceled it.
|
||||
// bsehr.error IS NULL <- this means the batch script framework didn't mark this host as incompatible
|
||||
// with this script run.
|
||||
batchScriptExecutionFilter += ` AND (hsr.exit_code IS NULL AND (hsr.canceled IS NULL OR hsr.canceled = 0) AND bsehr.error IS NULL)`
|
||||
case fleet.BatchScriptExecutionErrored:
|
||||
batchScriptExecutionFilter += ` AND hsr.exit_code > 0 AND hsr.canceled = 0`
|
||||
case fleet.BatchScriptExecutionIncompatible:
|
||||
batchScriptExecutionFilter += ` AND bsehr.error IS NOT NULL`
|
||||
case fleet.BatchScriptExecutionCanceled:
|
||||
batchScriptExecutionFilter += ` AND hsr.exit_code IS NULL AND hsr.canceled = 1`
|
||||
}
|
||||
}
|
||||
return batchScriptExecutionJoin, batchScriptExecutionFilter, whereParams
|
||||
}
|
||||
|
||||
func filterHostsByTeam(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
|
||||
if opt.TeamFilter == nil {
|
||||
// default "all teams" option
|
||||
|
||||
@@ -3470,9 +3470,37 @@ func testHostsListByBatchScriptExecutionStatus(t *testing.T, ds *Datastore) {
|
||||
require.Contains(t, expectedHostIds, hosts[0].ID)
|
||||
require.Contains(t, expectedHostIds, hosts[1].ID)
|
||||
|
||||
// Get the list of pending hosts. Check pagination by first getting a page of 1.
|
||||
batchHosts, meta, hostCount, err := ds.ListBatchScriptHosts(context.Background(), execID, fleet.BatchScriptExecutionPending, fleet.ListOptions{IncludeMetadata: true, PerPage: 1, Page: 0, OrderKey: "hostname", OrderDirection: fleet.OrderDescending})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, batchHosts, 1)
|
||||
require.Equal(t, uint(3), hostCount)
|
||||
require.Equal(t, host3.ID, batchHosts[0].ID)
|
||||
require.Equal(t, host3.Hostname, batchHosts[0].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionPending, batchHosts[0].Status)
|
||||
require.True(t, meta.HasNextResults)
|
||||
require.False(t, meta.HasPreviousResults)
|
||||
|
||||
// Get all of the pending hosts.
|
||||
batchHosts, meta, hostCount, err = ds.ListBatchScriptHosts(context.Background(), execID, fleet.BatchScriptExecutionPending, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, batchHosts, 3)
|
||||
require.Equal(t, uint(3), hostCount)
|
||||
require.Equal(t, host1.ID, batchHosts[0].ID)
|
||||
require.Equal(t, host1.Hostname, batchHosts[0].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionPending, batchHosts[0].Status)
|
||||
require.Equal(t, host2.ID, batchHosts[1].ID)
|
||||
require.Equal(t, host2.Hostname, batchHosts[1].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionPending, batchHosts[1].Status)
|
||||
require.Equal(t, host3.ID, batchHosts[2].ID)
|
||||
require.Equal(t, host3.Hostname, batchHosts[2].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionPending, batchHosts[2].Status)
|
||||
require.False(t, meta.HasNextResults)
|
||||
require.False(t, meta.HasPreviousResults)
|
||||
|
||||
// Do another batch script execution with the same hosts, and verify that "pending" returns correctly.
|
||||
// The SQL for retrieving "pending" hosts has to check both the host_script_results table (for hosts
|
||||
// that have "activated" the script acrivity) and the upcoming_activities table (for hosts that
|
||||
// that have "activated" the script activity) and the upcoming_activities table (for hosts that
|
||||
// have not yet activated the script activity).
|
||||
secondExecID, err := ds.BatchExecuteScript(ctx, &user.ID, script.ID, []uint{hostNoScripts.ID, hostWindows.ID, host1.ID, host2.ID, host3.ID})
|
||||
require.NoError(t, err)
|
||||
@@ -3542,6 +3570,54 @@ func testHostsListByBatchScriptExecutionStatus(t *testing.T, ds *Datastore) {
|
||||
|
||||
hosts = listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{BatchScriptExecutionIDFilter: &execID, BatchScriptExecutionStatusFilter: fleet.BatchScriptExecutionCanceled}, 1)
|
||||
require.Equal(t, host3.ID, hosts[0].ID)
|
||||
|
||||
// List pending hosts for this batch. There should be none.
|
||||
batchHosts, _, hostCount, err = ds.ListBatchScriptHosts(context.Background(), execID, fleet.BatchScriptExecutionPending, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, batchHosts, 0)
|
||||
require.Equal(t, uint(0), hostCount)
|
||||
|
||||
// List errored hosts for this batch. There should be one.
|
||||
batchHosts, _, hostCount, err = ds.ListBatchScriptHosts(context.Background(), execID, fleet.BatchScriptExecutionErrored, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, batchHosts, 1)
|
||||
require.Equal(t, uint(1), hostCount)
|
||||
require.Equal(t, host2.ID, batchHosts[0].ID)
|
||||
require.Equal(t, host2.Hostname, batchHosts[0].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionErrored, batchHosts[0].Status)
|
||||
require.Equal(t, host2Upcoming[0].ExecutionID, batchHosts[0].ScriptExecutionID)
|
||||
|
||||
// List ran hosts for this batch. There should be one.
|
||||
batchHosts, _, hostCount, err = ds.ListBatchScriptHosts(context.Background(), execID, fleet.BatchScriptExecutionRan, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, batchHosts, 1)
|
||||
require.Equal(t, uint(1), hostCount)
|
||||
require.Equal(t, host1.ID, batchHosts[0].ID)
|
||||
require.Equal(t, host1.Hostname, batchHosts[0].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionRan, batchHosts[0].Status)
|
||||
require.Equal(t, host1Upcoming[0].ExecutionID, batchHosts[0].ScriptExecutionID)
|
||||
require.Equal(t, "foo", batchHosts[0].ScriptOutput)
|
||||
|
||||
// List cancelled hosts for this batch. There should be one.
|
||||
batchHosts, _, hostCount, err = ds.ListBatchScriptHosts(context.Background(), execID, fleet.BatchScriptExecutionCanceled, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, batchHosts, 1)
|
||||
require.Equal(t, uint(1), hostCount)
|
||||
require.Equal(t, host3.ID, batchHosts[0].ID)
|
||||
require.Equal(t, host3.Hostname, batchHosts[0].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionCanceled, batchHosts[0].Status)
|
||||
|
||||
// List incompatible hosts for this batch. There should be two.
|
||||
batchHosts, _, hostCount, err = ds.ListBatchScriptHosts(context.Background(), execID, fleet.BatchScriptExecutionIncompatible, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, batchHosts, 2)
|
||||
require.Equal(t, uint(2), hostCount)
|
||||
require.Equal(t, hostNoScripts.ID, batchHosts[0].ID)
|
||||
require.Equal(t, hostNoScripts.Hostname, batchHosts[0].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionIncompatible, batchHosts[0].Status)
|
||||
require.Equal(t, hostWindows.ID, batchHosts[1].ID)
|
||||
require.Equal(t, hostWindows.Hostname, batchHosts[1].DisplayName)
|
||||
require.Equal(t, fleet.BatchScriptExecutionIncompatible, batchHosts[1].Status)
|
||||
}
|
||||
|
||||
func testHostsListMacOSSettingsDiskEncryptionStatus(t *testing.T, ds *Datastore) {
|
||||
|
||||
@@ -250,6 +250,7 @@ type Datastore interface {
|
||||
Host(ctx context.Context, id uint) (*Host, error)
|
||||
GetHostHealth(ctx context.Context, id uint) (*HostHealth, error)
|
||||
ListHosts(ctx context.Context, filter TeamFilter, opt HostListOptions) ([]*Host, error)
|
||||
ListBatchScriptHosts(ctx context.Context, batchScriptExecutionID string, batchScriptExecutionStatus BatchScriptExecutionStatus, opt ListOptions) ([]BatchScriptHost, *PaginationMetadata, uint, error)
|
||||
|
||||
// ListHostsLiteByUUIDs returns the "lite" version of hosts corresponding to
|
||||
// the provided uuids and filtered according to the provided team filters. It
|
||||
|
||||
@@ -282,6 +282,22 @@ func (hsr HostScriptResult) AuthzType() string {
|
||||
return "host_script_result"
|
||||
}
|
||||
|
||||
type BatchScriptHost struct {
|
||||
// ID is the host on which the script was executed.
|
||||
ID uint `json:"id" db:"id"`
|
||||
// Display name is the host's display name.
|
||||
DisplayName string `json:"display_name" db:"display_name"`
|
||||
// ExecutionID is a unique identifier for a single execution of the script.
|
||||
ScriptExecutionID string `json:"script_execution_id" db:"execution_id"`
|
||||
// Output is the combined stdout/stderr output of the script. It is empty
|
||||
// if no result was received yet.
|
||||
ScriptOutput string `json:"script_output_preview,omitempty" db:"output"`
|
||||
// Executed at is the time the script was executed on the host (if at all).
|
||||
ScriptExecutedAt *time.Time `json:"script_executed_at,omitempty" db:"updated_at"`
|
||||
// Status is the status of the host's batch script run.
|
||||
Status BatchScriptExecutionStatus `json:"script_status" db:"status"`
|
||||
}
|
||||
|
||||
// UserMessage returns the user-friendly message to associate with the current
|
||||
// state of the HostScriptResult. This is returned as part of the API endpoints
|
||||
// for running a script synchronously (so that fleetctl can display it) and to
|
||||
|
||||
@@ -1195,6 +1195,8 @@ type Service interface {
|
||||
|
||||
BatchScriptExecutionStatus(ctx context.Context, batchExecutionID string) (*BatchActivity, error)
|
||||
|
||||
BatchScriptExecutionHostResults(ctx context.Context, batchExecutionID string, status BatchScriptExecutionStatus, opt ListOptions) ([]BatchScriptHost, *PaginationMetadata, uint, error)
|
||||
|
||||
BatchScriptExecutionList(ctx context.Context, filter BatchExecutionStatusFilter) ([]BatchActivity, int64, error)
|
||||
|
||||
// BatchScriptCancel cancels a batch script execution
|
||||
|
||||
@@ -193,6 +193,8 @@ type GetHostHealthFunc func(ctx context.Context, id uint) (*fleet.HostHealth, er
|
||||
|
||||
type ListHostsFunc func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error)
|
||||
|
||||
type ListBatchScriptHostsFunc func(ctx context.Context, batchScriptExecutionID string, batchScriptExecutionStatus fleet.BatchScriptExecutionStatus, opt fleet.ListOptions) ([]fleet.BatchScriptHost, *fleet.PaginationMetadata, uint, error)
|
||||
|
||||
type ListHostsLiteByUUIDsFunc func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error)
|
||||
|
||||
type ListHostsLiteByIDsFunc func(ctx context.Context, ids []uint) ([]*fleet.Host, error)
|
||||
@@ -1725,6 +1727,9 @@ type DataStore struct {
|
||||
ListHostsFunc ListHostsFunc
|
||||
ListHostsFuncInvoked bool
|
||||
|
||||
ListBatchScriptHostsFunc ListBatchScriptHostsFunc
|
||||
ListBatchScriptHostsFuncInvoked bool
|
||||
|
||||
ListHostsLiteByUUIDsFunc ListHostsLiteByUUIDsFunc
|
||||
ListHostsLiteByUUIDsFuncInvoked bool
|
||||
|
||||
@@ -4237,6 +4242,13 @@ func (s *DataStore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt
|
||||
return s.ListHostsFunc(ctx, filter, opt)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListBatchScriptHosts(ctx context.Context, batchScriptExecutionID string, batchScriptExecutionStatus fleet.BatchScriptExecutionStatus, opt fleet.ListOptions) ([]fleet.BatchScriptHost, *fleet.PaginationMetadata, uint, error) {
|
||||
s.mu.Lock()
|
||||
s.ListBatchScriptHostsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ListBatchScriptHostsFunc(ctx, batchScriptExecutionID, batchScriptExecutionStatus, opt)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListHostsLiteByUUIDs(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
|
||||
s.mu.Lock()
|
||||
s.ListHostsLiteByUUIDsFuncInvoked = true
|
||||
|
||||
@@ -503,6 +503,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
ue.POST("/api/_version_/fleet/scripts/batch/{batch_execution_id:[a-zA-Z0-9-]+}/cancel", batchScriptCancelEndpoint, batchScriptCancelRequest{})
|
||||
// Deprecated, will remove in favor of batchScriptExecutionStatusEndpoint when batch script details page is ready.
|
||||
ue.GET("/api/_version_/fleet/scripts/batch/summary/{batch_execution_id:[a-zA-Z0-9-]+}", batchScriptExecutionSummaryEndpoint, batchScriptExecutionSummaryRequest{})
|
||||
ue.GET("/api/_version_/fleet/scripts/batch/{batch_execution_id:[a-zA-Z0-9-]+}/host-results", batchScriptExecutionHostResultsEndpoint, batchScriptExecutionHostResultsRequest{})
|
||||
ue.GET("/api/_version_/fleet/scripts/batch/{batch_execution_id:[a-zA-Z0-9-]+}", batchScriptExecutionStatusEndpoint, batchScriptExecutionStatusRequest{})
|
||||
ue.GET("/api/_version_/fleet/scripts/batch", batchScriptExecutionListEndpoint, batchScriptExecutionListRequest{})
|
||||
|
||||
|
||||
@@ -6661,6 +6661,20 @@ func (s *integrationEnterpriseTestSuite) TestRunBatchScript() {
|
||||
0,
|
||||
)
|
||||
|
||||
// List pending hosts
|
||||
var batchPendingHostsResp batchScriptExecutionHostResultsResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/scripts/batch/%s/host-results?status=pending", batchRes.BatchExecutionID), nil, http.StatusOK, &batchPendingHostsResp)
|
||||
require.Len(t, batchPendingHostsResp.Hosts, 2)
|
||||
require.Equal(t, batchPendingHostsResp.Count, uint(2))
|
||||
require.Equal(t, batchPendingHostsResp.Meta.HasNextResults, false)
|
||||
require.Equal(t, batchPendingHostsResp.Meta.HasPreviousResults, false)
|
||||
require.Equal(t, batchPendingHostsResp.Hosts[0].Status, fleet.BatchScriptExecutionPending)
|
||||
require.Equal(t, batchPendingHostsResp.Hosts[0].DisplayName, host1.DisplayName())
|
||||
require.Equal(t, batchPendingHostsResp.Hosts[0].ID, host1.ID)
|
||||
require.Equal(t, batchPendingHostsResp.Hosts[1].Status, fleet.BatchScriptExecutionPending)
|
||||
require.Equal(t, batchPendingHostsResp.Hosts[1].DisplayName, host2.DisplayName())
|
||||
require.Equal(t, batchPendingHostsResp.Hosts[1].ID, host2.ID)
|
||||
|
||||
// Another request so we can check the list endpoint
|
||||
var batchRes2 batchScriptRunResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/scripts/run/batch", batchScriptRunRequest{
|
||||
@@ -6674,8 +6688,8 @@ func (s *integrationEnterpriseTestSuite) TestRunBatchScript() {
|
||||
s.DoJSON("GET", "/api/latest/fleet/scripts/batch?team_id=0&per_page=1", nil, http.StatusOK, &batchListResp)
|
||||
require.Len(t, batchListResp.BatchScriptExecutions, 1)
|
||||
require.Equal(t, batchListResp.Count, uint(2))
|
||||
require.Equal(t, batchListResp.HasNextResults, true)
|
||||
require.Equal(t, batchListResp.HasPreviousResults, false)
|
||||
require.Equal(t, batchListResp.Meta.HasNextResults, true)
|
||||
require.Equal(t, batchListResp.Meta.HasPreviousResults, false)
|
||||
require.Equal(t, batchListResp.BatchScriptExecutions[0].BatchExecutionID, batchRes2.BatchExecutionID)
|
||||
require.Equal(t, *batchListResp.BatchScriptExecutions[0].ScriptID, script.ID)
|
||||
require.Equal(t, *batchListResp.BatchScriptExecutions[0].NumTargeted, uint(1))
|
||||
@@ -6684,8 +6698,8 @@ func (s *integrationEnterpriseTestSuite) TestRunBatchScript() {
|
||||
s.DoJSON("GET", "/api/latest/fleet/scripts/batch?team_id=0&page=1&per_page=1", nil, http.StatusOK, &batchListResp)
|
||||
require.Len(t, batchListResp.BatchScriptExecutions, 1)
|
||||
require.Equal(t, batchListResp.Count, uint(2))
|
||||
require.Equal(t, batchListResp.HasNextResults, false)
|
||||
require.Equal(t, batchListResp.HasPreviousResults, true)
|
||||
require.Equal(t, batchListResp.Meta.HasNextResults, false)
|
||||
require.Equal(t, batchListResp.Meta.HasPreviousResults, true)
|
||||
require.Equal(t, batchListResp.BatchScriptExecutions[0].BatchExecutionID, batchRes.BatchExecutionID)
|
||||
require.Equal(t, *batchListResp.BatchScriptExecutions[0].ScriptID, script.ID)
|
||||
require.Equal(t, *batchListResp.BatchScriptExecutions[0].NumTargeted, uint(2))
|
||||
|
||||
@@ -951,6 +951,8 @@ type batchSetScriptsResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r batchSetScriptsResponse) Error() error { return r.Err }
|
||||
|
||||
type batchScriptExecutionStatusRequest struct {
|
||||
BatchExecutionID string `url:"batch_execution_id"`
|
||||
}
|
||||
@@ -985,15 +987,28 @@ type (
|
||||
)
|
||||
|
||||
type batchScriptExecutionListResponse struct {
|
||||
BatchScriptExecutions []fleet.BatchActivity `json:"batch_executions"`
|
||||
Count uint `json:"count"`
|
||||
Err error `json:"error,omitempty"`
|
||||
fleet.PaginationMetadata
|
||||
BatchScriptExecutions []fleet.BatchActivity `json:"batch_executions"`
|
||||
Count uint `json:"count"`
|
||||
Err error `json:"error,omitempty"`
|
||||
Meta fleet.PaginationMetadata `json:"meta"`
|
||||
}
|
||||
|
||||
func (r batchScriptExecutionListResponse) Error() error { return r.Err }
|
||||
|
||||
func (r batchSetScriptsResponse) Error() error { return r.Err }
|
||||
type batchScriptExecutionHostResultsRequest struct {
|
||||
BatchExecutionID string `url:"batch_execution_id"`
|
||||
BatchExecutionStatus fleet.BatchScriptExecutionStatus `query:"status"`
|
||||
ListOptions fleet.ListOptions `url:"list_options"`
|
||||
}
|
||||
|
||||
type batchScriptExecutionHostResultsResponse struct {
|
||||
Hosts []fleet.BatchScriptHost `json:"hosts"`
|
||||
Count uint `json:"count"`
|
||||
Err error `json:"error,omitempty"`
|
||||
Meta fleet.PaginationMetadata `json:"meta"`
|
||||
}
|
||||
|
||||
func (r batchScriptExecutionHostResultsResponse) Error() error { return r.Err }
|
||||
|
||||
func batchSetScriptsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*batchSetScriptsRequest)
|
||||
@@ -1103,6 +1118,15 @@ func batchScriptExecutionSummaryEndpoint(ctx context.Context, request interface{
|
||||
}, nil
|
||||
}
|
||||
|
||||
func batchScriptExecutionHostResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*batchScriptExecutionHostResultsRequest)
|
||||
hosts, meta, count, err := svc.BatchScriptExecutionHostResults(ctx, req.BatchExecutionID, req.BatchExecutionStatus, req.ListOptions)
|
||||
if err != nil {
|
||||
return batchScriptExecutionHostResultsResponse{Err: err}, nil
|
||||
}
|
||||
return batchScriptExecutionHostResultsResponse{Hosts: hosts, Meta: *meta, Count: count}, nil
|
||||
}
|
||||
|
||||
func batchScriptExecutionStatusEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*batchScriptExecutionStatusRequest)
|
||||
status, err := svc.BatchScriptExecutionStatus(ctx, req.BatchExecutionID)
|
||||
@@ -1146,7 +1170,7 @@ func batchScriptExecutionListEndpoint(ctx context.Context, request interface{},
|
||||
return batchScriptExecutionListResponse{
|
||||
BatchScriptExecutions: list,
|
||||
Count: uint(count), //nolint:gosec // dismiss G115
|
||||
PaginationMetadata: fleet.PaginationMetadata{
|
||||
Meta: fleet.PaginationMetadata{
|
||||
HasNextResults: hasNextResults,
|
||||
HasPreviousResults: hasPreviousResults,
|
||||
},
|
||||
@@ -1279,6 +1303,55 @@ func (svc *Service) BatchScriptExecutionStatus(ctx context.Context, batchExecuti
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
func (svc *Service) BatchScriptExecutionHostResults(ctx context.Context, batchExecutionID string, status fleet.BatchScriptExecutionStatus, opt fleet.ListOptions) (hosts []fleet.BatchScriptHost, meta *fleet.PaginationMetadata, count uint, err error) {
|
||||
// Get the batch activity.
|
||||
batchActivity, err := svc.ds.GetBatchActivity(ctx, batchExecutionID)
|
||||
if err != nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return nil, nil, 0, ctxerr.Wrap(ctx, err, "getting batch activity")
|
||||
}
|
||||
if batchActivity.ScriptID == nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return nil, nil, 0, ctxerr.Wrap(ctx, err, "batch activity has no script ID")
|
||||
}
|
||||
|
||||
// Get the script referred to by the batch activity.
|
||||
script, err := svc.ds.Script(ctx, *batchActivity.ScriptID)
|
||||
if err != nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return nil, nil, 0, ctxerr.Wrap(ctx, err, "getting script")
|
||||
}
|
||||
if script == nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return nil, nil, 0, ctxerr.Wrap(ctx, err, "script not found")
|
||||
}
|
||||
|
||||
// Authorize based on the script's team ID.
|
||||
if err = svc.authz.Authorize(ctx, &fleet.Script{TeamID: script.TeamID}, fleet.ActionRead); err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
// Validate the supplied batch execution status.
|
||||
if !status.IsValid() {
|
||||
return nil, nil, 0, fleet.NewInvalidArgumentError("batch_execution_status", "invalid batch execution status")
|
||||
}
|
||||
|
||||
// Always include pagination info.
|
||||
opt.IncludeMetadata = true
|
||||
// Default sort order is name ascending.
|
||||
if opt.OrderKey == "" {
|
||||
opt.OrderKey = "display_name"
|
||||
opt.OrderDirection = fleet.OrderAscending
|
||||
}
|
||||
|
||||
hosts, meta, count, err = svc.ds.ListBatchScriptHosts(ctx, batchExecutionID, status, opt)
|
||||
if err != nil {
|
||||
return nil, nil, 0, ctxerr.Wrap(ctx, err, "list batch script hosts")
|
||||
}
|
||||
|
||||
return hosts, meta, count, nil
|
||||
}
|
||||
|
||||
func (svc *Service) BatchScriptExecutionList(ctx context.Context, filter fleet.BatchExecutionStatusFilter) ([]fleet.BatchActivity, int64, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Script{TeamID: filter.TeamID}, fleet.ActionRead); err != nil {
|
||||
return nil, 0, err
|
||||
|
||||
Reference in New Issue
Block a user