diff --git a/changes/27409-add-cancel-upcoming-activity-endpoint b/changes/27409-add-cancel-upcoming-activity-endpoint new file mode 100644 index 0000000000..419461c8b0 --- /dev/null +++ b/changes/27409-add-cancel-upcoming-activity-endpoint @@ -0,0 +1 @@ +* Added the `DELETE /api/latest/fleet/hosts/:id/activities/upcoming/:activity_id` endpoint to cancel an upcoming activity for a host. diff --git a/server/authz/policy.rego b/server/authz/policy.rego index e8e87884e4..82f0f93283 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -14,6 +14,7 @@ read := "read" list := "list" write := "write" write_host_label := "write_host_label" +cancel_host_activity := "cancel_host_activity" # User specific actions write_role := "write_role" @@ -266,20 +267,27 @@ allow { allowed_read_roles(action, base_roles, extra_roles)[_] == subject.global_role } -# Global gitops, admin and mantainers can write hosts. +# Global gitops, admin and maintainers can write hosts. allow { object.type == "host" subject.global_role == [admin, maintainer, gitops][_] action == write } -# Global admin, mantainers and gitops can write labels to hosts. +# Global admin, maintainers and gitops can write labels to hosts. allow { object.type == "host" subject.global_role == [admin, maintainer, gitops][_] action == write_host_label } +# Global admin and maintainers can cancel activities on a host. +allow { + object.type == "host" + subject.global_role == [admin, maintainer][_] + action == cancel_host_activity +} + # Allow read for global observer and observer_plus, selective_read for gitops. allow { object.type == "host" @@ -310,6 +318,13 @@ allow { action == write_host_label } +# Team admins and maintainers can cancel activities on a host of their own team. +allow { + object.type == "host" + team_role(subject, object.team_id) == [admin, maintainer][_] + action == cancel_host_activity +} + # Allow read for host health for global admin/maintainer, team admins, observer. allow { object.type == "host_health" diff --git a/server/authz/policy_test.go b/server/authz/policy_test.go index 164a8339f6..26782382fb 100644 --- a/server/authz/policy_test.go +++ b/server/authz/policy_test.go @@ -14,15 +14,16 @@ import ( ) const ( - read = fleet.ActionRead - list = fleet.ActionList - write = fleet.ActionWrite - writeRole = fleet.ActionWriteRole - run = fleet.ActionRun - runNew = fleet.ActionRunNew - changePwd = fleet.ActionChangePassword - selectiveRead = fleet.ActionSelectiveRead - selectiveList = fleet.ActionSelectiveList + read = fleet.ActionRead + list = fleet.ActionList + write = fleet.ActionWrite + writeRole = fleet.ActionWriteRole + run = fleet.ActionRun + runNew = fleet.ActionRunNew + changePwd = fleet.ActionChangePassword + selectiveRead = fleet.ActionSelectiveRead + selectiveList = fleet.ActionSelectiveList + cancelHostActivity = fleet.ActionCancelHostActivity ) var auth *Authorizer @@ -744,12 +745,15 @@ func TestAuthorizeHost(t *testing.T) { {user: nil, object: host, action: list, allow: false}, {user: nil, object: host, action: selectiveList, allow: false}, {user: nil, object: host, action: selectiveRead, allow: false}, + {user: nil, object: host, action: cancelHostActivity, allow: false}, {user: nil, object: hostTeam1, action: read, allow: false}, {user: nil, object: hostTeam1, action: write, allow: false}, {user: nil, object: hostTeam1, action: selectiveRead, allow: false}, + {user: nil, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: nil, object: hostTeam2, action: read, allow: false}, {user: nil, object: hostTeam2, action: write, allow: false}, {user: nil, object: hostTeam2, action: selectiveRead, allow: false}, + {user: nil, object: hostTeam2, action: cancelHostActivity, allow: false}, // No host access if the user has no roles. {user: test.UserNoRoles, object: host, action: read, allow: false}, @@ -757,12 +761,15 @@ func TestAuthorizeHost(t *testing.T) { {user: test.UserNoRoles, object: host, action: list, allow: false}, {user: test.UserNoRoles, object: host, action: selectiveList, allow: false}, {user: test.UserNoRoles, object: host, action: selectiveRead, allow: false}, + {user: test.UserNoRoles, object: host, action: cancelHostActivity, allow: false}, {user: test.UserNoRoles, object: hostTeam1, action: read, allow: false}, {user: test.UserNoRoles, object: hostTeam1, action: write, allow: false}, {user: test.UserNoRoles, object: hostTeam1, action: selectiveRead, allow: false}, + {user: test.UserNoRoles, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: test.UserNoRoles, object: hostTeam2, action: read, allow: false}, {user: test.UserNoRoles, object: hostTeam2, action: write, allow: false}, {user: test.UserNoRoles, object: hostTeam2, action: selectiveRead, allow: false}, + {user: test.UserNoRoles, object: hostTeam2, action: cancelHostActivity, allow: false}, // Global observer can read all {user: test.UserObserver, object: host, action: read, allow: true}, @@ -770,12 +777,15 @@ func TestAuthorizeHost(t *testing.T) { {user: test.UserObserver, object: host, action: list, allow: true}, {user: test.UserObserver, object: host, action: selectiveList, allow: true}, {user: test.UserObserver, object: host, action: selectiveRead, allow: true}, + {user: test.UserObserver, object: host, action: cancelHostActivity, allow: false}, {user: test.UserObserver, object: hostTeam1, action: read, allow: true}, {user: test.UserObserver, object: hostTeam1, action: selectiveRead, allow: true}, {user: test.UserObserver, object: hostTeam1, action: write, allow: false}, + {user: test.UserObserver, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: test.UserObserver, object: hostTeam2, action: read, allow: true}, {user: test.UserObserver, object: hostTeam2, action: selectiveRead, allow: true}, {user: test.UserObserver, object: hostTeam2, action: write, allow: false}, + {user: test.UserObserver, object: hostTeam2, action: cancelHostActivity, allow: false}, // Global observer+ can read all {user: test.UserObserverPlus, object: host, action: read, allow: true}, @@ -783,12 +793,15 @@ func TestAuthorizeHost(t *testing.T) { {user: test.UserObserverPlus, object: host, action: list, allow: true}, {user: test.UserObserverPlus, object: host, action: selectiveList, allow: true}, {user: test.UserObserverPlus, object: host, action: selectiveRead, allow: true}, + {user: test.UserObserverPlus, object: host, action: cancelHostActivity, allow: false}, {user: test.UserObserverPlus, object: hostTeam1, action: read, allow: true}, {user: test.UserObserverPlus, object: hostTeam1, action: selectiveRead, allow: true}, {user: test.UserObserverPlus, object: hostTeam1, action: write, allow: false}, + {user: test.UserObserverPlus, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: test.UserObserverPlus, object: hostTeam2, action: read, allow: true}, {user: test.UserObserverPlus, object: hostTeam2, action: selectiveRead, allow: true}, {user: test.UserObserverPlus, object: hostTeam2, action: write, allow: false}, + {user: test.UserObserverPlus, object: hostTeam2, action: cancelHostActivity, allow: false}, // Global admin can read/write all {user: test.UserAdmin, object: host, action: read, allow: true}, @@ -796,12 +809,15 @@ func TestAuthorizeHost(t *testing.T) { {user: test.UserAdmin, object: host, action: write, allow: true}, {user: test.UserAdmin, object: host, action: list, allow: true}, {user: test.UserAdmin, object: host, action: selectiveList, allow: true}, + {user: test.UserAdmin, object: host, action: cancelHostActivity, allow: true}, {user: test.UserAdmin, object: hostTeam1, action: read, allow: true}, {user: test.UserAdmin, object: hostTeam1, action: selectiveRead, allow: true}, {user: test.UserAdmin, object: hostTeam1, action: write, allow: true}, + {user: test.UserAdmin, object: hostTeam1, action: cancelHostActivity, allow: true}, {user: test.UserAdmin, object: hostTeam2, action: read, allow: true}, {user: test.UserAdmin, object: hostTeam2, action: selectiveRead, allow: true}, {user: test.UserAdmin, object: hostTeam2, action: write, allow: true}, + {user: test.UserAdmin, object: hostTeam2, action: cancelHostActivity, allow: true}, // Global maintainer can read/write all {user: test.UserMaintainer, object: host, action: read, allow: true}, @@ -809,12 +825,15 @@ func TestAuthorizeHost(t *testing.T) { {user: test.UserMaintainer, object: host, action: write, allow: true}, {user: test.UserMaintainer, object: host, action: list, allow: true}, {user: test.UserMaintainer, object: host, action: selectiveList, allow: true}, + {user: test.UserMaintainer, object: host, action: cancelHostActivity, allow: true}, {user: test.UserMaintainer, object: hostTeam1, action: read, allow: true}, {user: test.UserMaintainer, object: hostTeam1, action: selectiveRead, allow: true}, {user: test.UserMaintainer, object: hostTeam1, action: write, allow: true}, + {user: test.UserMaintainer, object: hostTeam1, action: cancelHostActivity, allow: true}, {user: test.UserMaintainer, object: hostTeam2, action: read, allow: true}, {user: test.UserMaintainer, object: hostTeam2, action: selectiveRead, allow: true}, {user: test.UserMaintainer, object: hostTeam2, action: write, allow: true}, + {user: test.UserMaintainer, object: hostTeam2, action: cancelHostActivity, allow: true}, // Global GitOps can write and selectively read all. {user: test.UserGitOps, object: host, action: read, allow: false}, @@ -822,12 +841,15 @@ func TestAuthorizeHost(t *testing.T) { {user: test.UserGitOps, object: host, action: selectiveRead, allow: true}, {user: test.UserGitOps, object: host, action: list, allow: false}, {user: test.UserGitOps, object: host, action: selectiveList, allow: true}, + {user: test.UserGitOps, object: host, action: cancelHostActivity, allow: false}, {user: test.UserGitOps, object: hostTeam1, action: read, allow: false}, {user: test.UserGitOps, object: hostTeam1, action: write, allow: true}, {user: test.UserGitOps, object: hostTeam1, action: selectiveRead, allow: true}, + {user: test.UserGitOps, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: test.UserGitOps, object: hostTeam2, action: read, allow: false}, {user: test.UserGitOps, object: hostTeam2, action: write, allow: true}, {user: test.UserGitOps, object: hostTeam2, action: selectiveRead, allow: true}, + {user: test.UserGitOps, object: hostTeam2, action: cancelHostActivity, allow: false}, // Team observer can read only on appropriate team {user: teamObserver, object: host, action: read, allow: false}, @@ -835,12 +857,15 @@ func TestAuthorizeHost(t *testing.T) { {user: teamObserver, object: host, action: write, allow: false}, {user: teamObserver, object: host, action: list, allow: true}, {user: teamObserver, object: host, action: selectiveList, allow: true}, + {user: teamObserver, object: host, action: cancelHostActivity, allow: false}, {user: teamObserver, object: hostTeam1, action: read, allow: true}, {user: teamObserver, object: hostTeam1, action: selectiveRead, allow: true}, {user: teamObserver, object: hostTeam1, action: write, allow: false}, + {user: teamObserver, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: teamObserver, object: hostTeam2, action: read, allow: false}, {user: teamObserver, object: hostTeam2, action: selectiveRead, allow: false}, {user: teamObserver, object: hostTeam2, action: write, allow: false}, + {user: teamObserver, object: hostTeam2, action: cancelHostActivity, allow: false}, // Team observer+ can read only on appropriate team {user: teamObserverPlus, object: host, action: read, allow: false}, @@ -848,12 +873,15 @@ func TestAuthorizeHost(t *testing.T) { {user: teamObserverPlus, object: host, action: write, allow: false}, {user: teamObserverPlus, object: host, action: list, allow: true}, {user: teamObserverPlus, object: host, action: selectiveList, allow: true}, + {user: teamObserverPlus, object: host, action: cancelHostActivity, allow: false}, {user: teamObserverPlus, object: hostTeam1, action: read, allow: true}, {user: teamObserverPlus, object: hostTeam1, action: selectiveRead, allow: true}, {user: teamObserverPlus, object: hostTeam1, action: write, allow: false}, + {user: teamObserverPlus, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: teamObserverPlus, object: hostTeam2, action: read, allow: false}, {user: teamObserverPlus, object: hostTeam2, action: selectiveRead, allow: false}, {user: teamObserverPlus, object: hostTeam2, action: write, allow: false}, + {user: teamObserverPlus, object: hostTeam2, action: cancelHostActivity, allow: false}, // Team maintainer can read/write only on appropriate team {user: teamMaintainer, object: host, action: read, allow: false}, @@ -861,11 +889,14 @@ func TestAuthorizeHost(t *testing.T) { {user: teamMaintainer, object: host, action: write, allow: false}, {user: teamMaintainer, object: host, action: list, allow: true}, {user: teamMaintainer, object: host, action: selectiveList, allow: true}, + {user: teamMaintainer, object: host, action: cancelHostActivity, allow: false}, {user: teamMaintainer, object: hostTeam1, action: read, allow: true}, {user: teamMaintainer, object: hostTeam1, action: selectiveRead, allow: true}, {user: teamMaintainer, object: hostTeam1, action: write, allow: true}, + {user: teamMaintainer, object: hostTeam1, action: cancelHostActivity, allow: true}, {user: teamMaintainer, object: hostTeam2, action: read, allow: false}, {user: teamMaintainer, object: hostTeam2, action: write, allow: false}, + {user: teamMaintainer, object: hostTeam2, action: cancelHostActivity, allow: false}, // Team admin can read/write only on appropriate team {user: teamAdmin, object: host, action: read, allow: false}, @@ -873,23 +904,29 @@ func TestAuthorizeHost(t *testing.T) { {user: teamAdmin, object: host, action: write, allow: false}, {user: teamAdmin, object: host, action: list, allow: true}, {user: teamAdmin, object: host, action: selectiveList, allow: true}, + {user: teamAdmin, object: host, action: cancelHostActivity, allow: false}, {user: teamAdmin, object: hostTeam1, action: read, allow: true}, {user: teamAdmin, object: hostTeam1, action: write, allow: true}, + {user: teamAdmin, object: hostTeam1, action: cancelHostActivity, allow: true}, {user: teamAdmin, object: hostTeam2, action: read, allow: false}, {user: teamAdmin, object: hostTeam2, action: write, allow: false}, + {user: teamAdmin, object: hostTeam2, action: cancelHostActivity, allow: false}, // Team GitOps can cannot read hosts, but it can write and selectively read them. {user: teamGitOps, object: host, action: read, allow: false}, {user: teamGitOps, object: host, action: write, allow: false}, {user: teamGitOps, object: host, action: selectiveRead, allow: false}, + {user: teamGitOps, object: host, action: cancelHostActivity, allow: false}, {user: teamGitOps, object: hostTeam1, action: read, allow: false}, {user: teamGitOps, object: hostTeam1, action: list, allow: false}, {user: teamGitOps, object: hostTeam1, action: selectiveList, allow: true}, {user: teamGitOps, object: hostTeam1, action: selectiveRead, allow: true}, {user: teamGitOps, object: hostTeam1, action: write, allow: false}, + {user: teamGitOps, object: hostTeam1, action: cancelHostActivity, allow: false}, {user: teamGitOps, object: hostTeam2, action: read, allow: false}, {user: teamGitOps, object: hostTeam2, action: write, allow: false}, {user: teamGitOps, object: hostTeam2, action: selectiveRead, allow: false}, + {user: teamGitOps, object: hostTeam2, action: cancelHostActivity, allow: false}, }) } diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go index 62fa3e61d6..90b7849515 100644 --- a/server/datastore/mysql/activities.go +++ b/server/datastore/mysql/activities.go @@ -667,6 +667,10 @@ func (ds *Datastore) CleanupActivitiesAndAssociatedData(ctx context.Context, max return nil } +func (ds *Datastore) CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error { + panic("unimplemented") +} + // This function activates the next upcoming activity, if any, for the specified host. // It does a few things to achieve this: // - If there was an activity already marked as activated (activated_at is diff --git a/server/datastore/mysql/migrations/tables/20250331154206_AddCanceledFieldForUpcomingActivities.go b/server/datastore/mysql/migrations/tables/20250331154206_AddCanceledFieldForUpcomingActivities.go new file mode 100644 index 0000000000..c966a28204 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20250331154206_AddCanceledFieldForUpcomingActivities.go @@ -0,0 +1,115 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20250331154206, Down_20250331154206) +} + +func Up_20250331154206(tx *sql.Tx) error { + _, err := tx.Exec(` +ALTER TABLE host_script_results + ADD COLUMN canceled TINYINT(1) NOT NULL DEFAULT '0' +`) + if err != nil { + return fmt.Errorf("failed to alter host_script_results: %w", err) + } + + _, err = tx.Exec(` +ALTER TABLE host_vpp_software_installs + ADD COLUMN canceled TINYINT(1) NOT NULL DEFAULT '0' +`) + if err != nil { + return fmt.Errorf("failed to alter host_vpp_software_installs: %w", err) + } + + if _, err := tx.Exec(` +ALTER TABLE host_software_installs + ADD COLUMN canceled TINYINT(1) NOT NULL DEFAULT '0', + CHANGE COLUMN execution_status execution_status + ENUM('pending_install', 'failed_install', 'installed', 'pending_uninstall', 'failed_uninstall', 'canceled_install', 'canceled_uninstall') +GENERATED ALWAYS AS ( +CASE + WHEN canceled = 1 AND uninstall = 0 THEN 'canceled_install' + + WHEN canceled = 1 AND uninstall = 1 THEN 'canceled_uninstall' + + WHEN post_install_script_exit_code IS NOT NULL AND + post_install_script_exit_code = 0 THEN 'installed' + + WHEN post_install_script_exit_code IS NOT NULL AND + post_install_script_exit_code != 0 THEN 'failed_install' + + WHEN install_script_exit_code IS NOT NULL AND + install_script_exit_code = 0 THEN 'installed' + + WHEN install_script_exit_code IS NOT NULL AND + install_script_exit_code != 0 THEN 'failed_install' + + WHEN pre_install_query_output IS NOT NULL AND + pre_install_query_output = '' THEN 'failed_install' + + WHEN host_id IS NOT NULL AND uninstall = 0 THEN 'pending_install' + + WHEN uninstall_script_exit_code IS NOT NULL AND + uninstall_script_exit_code != 0 THEN 'failed_uninstall' + + WHEN uninstall_script_exit_code IS NOT NULL AND + uninstall_script_exit_code = 0 THEN NULL -- available for install again + + WHEN host_id IS NOT NULL AND uninstall = 1 THEN 'pending_uninstall' + + ELSE NULL -- not installed from Fleet installer or successfully uninstalled +END +) VIRTUAL NULL, + + CHANGE COLUMN status status + ENUM('pending_install', 'failed_install', 'installed', 'pending_uninstall', 'failed_uninstall', 'canceled_install', 'canceled_uninstall') +GENERATED ALWAYS AS ( +CASE + WHEN removed = 1 THEN NULL + + WHEN canceled = 1 AND uninstall = 0 THEN 'canceled_install' + + WHEN canceled = 1 AND uninstall = 1 THEN 'canceled_uninstall' + + WHEN post_install_script_exit_code IS NOT NULL AND + post_install_script_exit_code = 0 THEN 'installed' + + WHEN post_install_script_exit_code IS NOT NULL AND + post_install_script_exit_code != 0 THEN 'failed_install' + + WHEN install_script_exit_code IS NOT NULL AND + install_script_exit_code = 0 THEN 'installed' + + WHEN install_script_exit_code IS NOT NULL AND + install_script_exit_code != 0 THEN 'failed_install' + + WHEN pre_install_query_output IS NOT NULL AND + pre_install_query_output = '' THEN 'failed_install' + + WHEN host_id IS NOT NULL AND uninstall = 0 THEN 'pending_install' + + WHEN uninstall_script_exit_code IS NOT NULL AND + uninstall_script_exit_code != 0 THEN 'failed_uninstall' + + WHEN uninstall_script_exit_code IS NOT NULL AND + uninstall_script_exit_code = 0 THEN NULL -- available for install again + + WHEN host_id IS NOT NULL AND uninstall = 1 THEN 'pending_uninstall' + + ELSE NULL -- not installed from Fleet installer or successfully uninstalled +END +) STORED NULL +`); err != nil { + return fmt.Errorf("failed to add canceled column and update statuses generated columns on host_software_installs: %w", err) + } + return nil +} + +func Down_20250331154206(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20250331154206_AddCanceledFieldForUpcomingActivities_test.go b/server/datastore/mysql/migrations/tables/20250331154206_AddCanceledFieldForUpcomingActivities_test.go new file mode 100644 index 0000000000..a6c9441262 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20250331154206_AddCanceledFieldForUpcomingActivities_test.go @@ -0,0 +1,92 @@ +package tables + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20250331154206(t *testing.T) { + db := applyUpToPrev(t) + + // this is basically the same test as 20241021224359_AddExecutionStatusToHostSoftwareInstalls_test.go + // to ensure that the same state still corresponds to the same resulting statuses, after migration + // and change of the status and execution_status columns. + hostID := insertHost(t, db, nil) + dataStmts := ` + INSERT INTO script_contents (id, md5_checksum, contents) VALUES + (1, 'checksum', 'script content'); + + INSERT INTO software_titles (id, name, source, browser) VALUES (1, 'Foo.app', 'apps', ''); + + INSERT INTO software_installers + (id, title_id, filename, version, platform, install_script_content_id, storage_id, package_ids, uninstall_script_content_id) + VALUES + (1, 1, 'foo-installer.pkg', '1.1', 'darwin', 1, 'storage-id', '', 1); + ` + _, err := db.Exec(dataStmts) + require.NoError(t, err) + + hsiStmt := ` + INSERT INTO host_software_installs ( + host_id, + execution_id, + software_installer_id, + install_script_exit_code, + uninstall_script_exit_code, + updated_at, + uninstall, + removed + ) VALUES (?, ?, ?, ?, ?, '2024-10-01 00:00:00', ?, 1)` + hsiInstall := execNoErrLastID(t, db, hsiStmt, hostID, "execution-id1", 1, 0, nil, 0) + hsiUninstall := execNoErrLastID(t, db, hsiStmt, hostID, "execution-id2", 1, nil, 0, 1) + + // Apply current migration. + applyNext(t, db) + + var statuses struct { + Status *string `db:"status"` + ExecutionStatus *string `db:"execution_status"` + } + + err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiInstall) + require.NoError(t, err) + require.NotNil(t, statuses.ExecutionStatus) + require.Equal(t, "installed", *statuses.ExecutionStatus) + require.Nil(t, statuses.Status) + + err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiUninstall) + require.NoError(t, err) + require.Nil(t, statuses.ExecutionStatus) // uninstalls have null status + require.Nil(t, statuses.Status) + + execNoErr(t, db, `UPDATE host_software_installs SET removed = 0`) + + err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiInstall) + require.NoError(t, err) + require.NotNil(t, statuses.ExecutionStatus) + require.Equal(t, "installed", *statuses.ExecutionStatus) + require.NotNil(t, statuses.Status) + require.Equal(t, "installed", *statuses.Status) + + err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiUninstall) + require.NoError(t, err) + require.Nil(t, statuses.ExecutionStatus) // uninstalls have null status + require.Nil(t, statuses.Status) + + execNoErr(t, db, `UPDATE host_software_installs SET canceled = 1`) + + err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiInstall) + require.NoError(t, err) + require.NotNil(t, statuses.ExecutionStatus) + require.Equal(t, "canceled_install", *statuses.ExecutionStatus) + require.NotNil(t, statuses.Status) + require.Equal(t, "canceled_install", *statuses.Status) + + err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiUninstall) + require.NoError(t, err) + require.NotNil(t, statuses.ExecutionStatus) + require.Equal(t, "canceled_uninstall", *statuses.ExecutionStatus) + require.NotNil(t, statuses.Status) + require.Equal(t, "canceled_uninstall", *statuses.Status) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 1d21c54f98..c0edaa90bf 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -651,6 +651,7 @@ CREATE TABLE `host_script_results` ( `policy_id` int unsigned DEFAULT NULL, `setup_experience_script_id` int unsigned DEFAULT NULL, `is_internal` tinyint(1) DEFAULT '0', + `canceled` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_host_script_results_execution_id` (`execution_id`), KEY `idx_host_script_results_host_exit_created` (`host_id`,`exit_code`,`created_at`), @@ -719,13 +720,14 @@ CREATE TABLE `host_software_installs` ( `uninstall_script_output` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, `uninstall_script_exit_code` int DEFAULT NULL, `uninstall` tinyint unsigned NOT NULL DEFAULT '0', - `status` enum('pending_install','failed_install','installed','pending_uninstall','failed_uninstall') COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS ((case when (`removed` = 1) then NULL when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`pre_install_query_output` is not null) and (`pre_install_query_output` = _utf8mb4'')) then _utf8mb4'failed_install' when ((`host_id` is not null) and (`uninstall` = 0)) then _utf8mb4'pending_install' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` <> 0)) then _utf8mb4'failed_uninstall' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` = 0)) then NULL when ((`host_id` is not null) and (`uninstall` = 1)) then _utf8mb4'pending_uninstall' else NULL end)) STORED, + `status` enum('pending_install','failed_install','installed','pending_uninstall','failed_uninstall','canceled_install','canceled_uninstall') COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS ((case when (`removed` = 1) then NULL when ((`canceled` = 1) and (`uninstall` = 0)) then _utf8mb4'canceled_install' when ((`canceled` = 1) and (`uninstall` = 1)) then _utf8mb4'canceled_uninstall' when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`pre_install_query_output` is not null) and (`pre_install_query_output` = _utf8mb4'')) then _utf8mb4'failed_install' when ((`host_id` is not null) and (`uninstall` = 0)) then _utf8mb4'pending_install' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` <> 0)) then _utf8mb4'failed_uninstall' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` = 0)) then NULL when ((`host_id` is not null) and (`uninstall` = 1)) then _utf8mb4'pending_uninstall' else NULL end)) STORED, `policy_id` int unsigned DEFAULT NULL, `installer_filename` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '[deleted installer]', `version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'unknown', `software_title_id` int unsigned DEFAULT NULL, `software_title_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '[deleted title]', - `execution_status` enum('pending_install','failed_install','installed','pending_uninstall','failed_uninstall') COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS ((case when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`pre_install_query_output` is not null) and (`pre_install_query_output` = _utf8mb4'')) then _utf8mb4'failed_install' when ((`host_id` is not null) and (`uninstall` = 0)) then _utf8mb4'pending_install' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` <> 0)) then _utf8mb4'failed_uninstall' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` = 0)) then NULL when ((`host_id` is not null) and (`uninstall` = 1)) then _utf8mb4'pending_uninstall' else NULL end)) VIRTUAL, + `execution_status` enum('pending_install','failed_install','installed','pending_uninstall','failed_uninstall','canceled_install','canceled_uninstall') COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS ((case when ((`canceled` = 1) and (`uninstall` = 0)) then _utf8mb4'canceled_install' when ((`canceled` = 1) and (`uninstall` = 1)) then _utf8mb4'canceled_uninstall' when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`post_install_script_exit_code` is not null) and (`post_install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` = 0)) then _utf8mb4'installed' when ((`install_script_exit_code` is not null) and (`install_script_exit_code` <> 0)) then _utf8mb4'failed_install' when ((`pre_install_query_output` is not null) and (`pre_install_query_output` = _utf8mb4'')) then _utf8mb4'failed_install' when ((`host_id` is not null) and (`uninstall` = 0)) then _utf8mb4'pending_install' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` <> 0)) then _utf8mb4'failed_uninstall' when ((`uninstall_script_exit_code` is not null) and (`uninstall_script_exit_code` = 0)) then NULL when ((`host_id` is not null) and (`uninstall` = 1)) then _utf8mb4'pending_uninstall' else NULL end)) VIRTUAL, + `canceled` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_host_software_installs_execution_id` (`execution_id`), KEY `fk_host_software_installs_user_id` (`user_id`), @@ -777,6 +779,7 @@ CREATE TABLE `host_vpp_software_installs` ( `removed` tinyint NOT NULL DEFAULT '0', `vpp_token_id` int unsigned DEFAULT NULL, `policy_id` int unsigned DEFAULT NULL, + `canceled` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_host_vpp_software_installs_command_uuid` (`command_uuid`), KEY `user_id` (`user_id`), @@ -1217,9 +1220,9 @@ CREATE TABLE `migration_status_tables` ( `is_applied` tinyint(1) NOT NULL, `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=372 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=373 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250325122638,1,'2020-01-01 01:01:01'),(370,20250326161930,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250325122638,1,'2020-01-01 01:01:01'),(370,20250326161930,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'),(372,20250331154206,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/authz.go b/server/fleet/authz.go index 811aad2366..92ab377753 100644 --- a/server/fleet/authz.go +++ b/server/fleet/authz.go @@ -9,6 +9,8 @@ const ( ActionWrite = "write" // ActionWriteHostLabel refers to writing labels on hosts. ActionWriteHostLabel = "write_host_label" + // ActionCancelHostActivity refers to canceling an upcoming activity on a host. + ActionCancelHostActivity = "cancel_host_activity" // // User specific actions diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 77a01eb50f..4de568be07 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -679,6 +679,7 @@ type Datastore interface { ListActivities(ctx context.Context, opt ListActivitiesOptions) ([]*Activity, *PaginationMetadata, error) MarkActivitiesAsStreamed(ctx context.Context, activityIDs []uint) error ListHostUpcomingActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*UpcomingActivity, *PaginationMetadata, error) + CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error ListHostPastActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*Activity, *PaginationMetadata, error) IsExecutionPendingForHost(ctx context.Context, hostID uint, scriptID uint) (bool, error) diff --git a/server/fleet/service.go b/server/fleet/service.go index 199469ee3e..d9cdfe33e6 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -602,6 +602,11 @@ type Service interface { // ListHostPastActivities lists the activities that have already happened for the specified host. ListHostPastActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*Activity, *PaginationMetadata, error) + // CancelHostUpcomingActivity cancels an upcoming activity for the specified + // host. If the activity does not exist in the queue of upcoming activities + // (e.g. it did complete), it returns a not found error. + CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error + // ///////////////////////////////////////////////////////////////////////////// // UserRolesService diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 543abddd85..038d407666 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -498,6 +498,8 @@ type MarkActivitiesAsStreamedFunc func(ctx context.Context, activityIDs []uint) type ListHostUpcomingActivitiesFunc func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.UpcomingActivity, *fleet.PaginationMetadata, error) +type CancelHostUpcomingActivityFunc func(ctx context.Context, hostID uint, upcomingActivityID string) error + type ListHostPastActivitiesFunc func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) type IsExecutionPendingForHostFunc func(ctx context.Context, hostID uint, scriptID uint) (bool, error) @@ -714,7 +716,7 @@ type ReplaceHostBatteriesFunc func(ctx context.Context, id uint, mappings []*fle type VerifyEnrollSecretFunc func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) -type IsEnrollSecretAvailableFunc func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) +type IsEnrollSecretAvailableFunc func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) type EnrollHostFunc func(ctx context.Context, isMDMEnabled bool, osqueryHostId string, hardwareUUID string, hardwareSerial string, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) @@ -2011,6 +2013,9 @@ type DataStore struct { ListHostUpcomingActivitiesFunc ListHostUpcomingActivitiesFunc ListHostUpcomingActivitiesFuncInvoked bool + CancelHostUpcomingActivityFunc CancelHostUpcomingActivityFunc + CancelHostUpcomingActivityFuncInvoked bool + ListHostPastActivitiesFunc ListHostPastActivitiesFunc ListHostPastActivitiesFuncInvoked bool @@ -4877,6 +4882,13 @@ func (s *DataStore) ListHostUpcomingActivities(ctx context.Context, hostID uint, return s.ListHostUpcomingActivitiesFunc(ctx, hostID, opt) } +func (s *DataStore) CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error { + s.mu.Lock() + s.CancelHostUpcomingActivityFuncInvoked = true + s.mu.Unlock() + return s.CancelHostUpcomingActivityFunc(ctx, hostID, upcomingActivityID) +} + func (s *DataStore) ListHostPastActivities(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) { s.mu.Lock() s.ListHostPastActivitiesFuncInvoked = true @@ -5633,11 +5645,11 @@ func (s *DataStore) VerifyEnrollSecret(ctx context.Context, secret string) (*fle return s.VerifyEnrollSecretFunc(ctx, secret) } -func (s *DataStore) IsEnrollSecretAvailable(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) { +func (s *DataStore) IsEnrollSecretAvailable(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) { s.mu.Lock() s.IsEnrollSecretAvailableFuncInvoked = true s.mu.Unlock() - return s.IsEnrollSecretAvailableFunc(ctx, secret, new, teamID) + return s.IsEnrollSecretAvailableFunc(ctx, secret, isNew, teamID) } func (s *DataStore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryHostId string, hardwareUUID string, hardwareSerial string, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) { diff --git a/server/service/activities.go b/server/service/activities.go index 84877190c3..80baa7afb8 100644 --- a/server/service/activities.go +++ b/server/service/activities.go @@ -244,3 +244,45 @@ func (svc *Service) ListHostPastActivities(ctx context.Context, hostID uint, opt return svc.ds.ListHostPastActivities(ctx, hostID, opt) } + +//////////////////////////////////////////////////////////////////////////////// +// Cancel host upcoming activity +//////////////////////////////////////////////////////////////////////////////// + +type cancelHostUpcomingActivityRequest struct { + HostID uint `url:"id"` + ActivityID string `url:"activity_id"` +} + +type cancelHostUpcomingActivityResponse struct { + Err error `json:"error,omitempty"` +} + +func (r cancelHostUpcomingActivityResponse) Error() error { return r.Err } +func (r cancelHostUpcomingActivityResponse) Status() int { return http.StatusNoContent } + +func cancelHostUpcomingActivityEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { + req := request.(*cancelHostUpcomingActivityRequest) + err := svc.CancelHostUpcomingActivity(ctx, req.HostID, req.ActivityID) + if err != nil { + return cancelHostUpcomingActivityResponse{Err: err}, nil + } + return cancelHostUpcomingActivityResponse{}, nil +} + +func (svc *Service) CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error { + // First ensure the user has access to list hosts, then check the specific + // host once team_id is loaded. + if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { + return err + } + host, err := svc.ds.HostLite(ctx, hostID) + if err != nil { + return ctxerr.Wrap(ctx, err, "get host") + } + // Authorize again with team loaded now that we have team_id + if err := svc.authz.Authorize(ctx, host, fleet.ActionCancelHostActivity); err != nil { + return err + } + return svc.ds.CancelHostUpcomingActivity(ctx, hostID, upcomingActivityID) +} diff --git a/server/service/activities_test.go b/server/service/activities_test.go index d2042280ef..c4fd06ad7f 100644 --- a/server/service/activities_test.go +++ b/server/service/activities_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/fleetdm/fleet/v4/server/authz" + "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" @@ -364,3 +365,117 @@ func TestActivityWebhooksDisabled(t *testing.T) { require.True(t, ds.NewActivityFuncInvoked) assert.Equal(t, user, activityUser) } + +func TestCancelHostUpcomingActivityAuth(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}) + + const ( + teamHostID = 1 + globalHostID = 2 + ) + + teamHost := &fleet.Host{TeamID: ptr.Uint(1), Platform: "darwin"} + globalHost := &fleet.Host{Platform: "darwin"} + + ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) { + if hostID == teamHostID { + return teamHost, nil + } + return globalHost, nil + } + ds.CancelHostUpcomingActivityFunc = func(ctx context.Context, hostID uint, actID string) error { + return nil + } + + cases := []struct { + name string + user *fleet.User + shouldFailGlobal bool + shouldFailTeam bool + }{ + { + name: "global observer", + user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + { + name: "team observer", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + { + name: "global observer plus", + user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + { + name: "team observer plus", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + { + name: "global admin", + user: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + shouldFailGlobal: false, + shouldFailTeam: false, + }, + { + name: "team admin", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}, + shouldFailGlobal: true, + shouldFailTeam: false, + }, + { + name: "global maintainer", + user: &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, + shouldFailGlobal: false, + shouldFailTeam: false, + }, + { + name: "team maintainer", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}}, + shouldFailGlobal: true, + shouldFailTeam: false, + }, + { + name: "team admin wrong team", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 42}, Role: fleet.RoleAdmin}}}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + { + name: "team maintainer wrong team", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 42}, Role: fleet.RoleMaintainer}}}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + { + name: "global gitops", + user: &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + { + name: "team gitops", + user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}}, + shouldFailGlobal: true, + shouldFailTeam: true, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) + + err := svc.CancelHostUpcomingActivity(ctx, globalHostID, "abc") + checkAuthErr(t, tt.shouldFailGlobal, err) + err = svc.CancelHostUpcomingActivity(ctx, teamHostID, "abc") + checkAuthErr(t, tt.shouldFailTeam, err) + }) + } +} diff --git a/server/service/handler.go b/server/service/handler.go index 25aa84fbe2..d9dfd5ae5f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -480,6 +480,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/scripts", getHostScriptDetailsEndpoint, getHostScriptDetailsRequest{}) ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/activities/upcoming", listHostUpcomingActivitiesEndpoint, listHostUpcomingActivitiesRequest{}) ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/activities", listHostPastActivitiesEndpoint, listHostPastActivitiesRequest{}) + ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/activities/upcoming/{activity_id}", cancelHostUpcomingActivityEndpoint, cancelHostUpcomingActivityRequest{}) ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/lock", lockHostEndpoint, lockHostRequest{}) ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/unlock", unlockHostEndpoint, unlockHostRequest{}) ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/wipe", wipeHostEndpoint, wipeHostRequest{})