diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index d2a9c15..5eae8ff 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -99,12 +99,38 @@ func fileStorageConfig(dsn, options string) (*file.FileStorage, error) { } func mysqlStorageConfig(dsn, options string, logger log.Logger) (*mysql.MySQLStorage, error) { - if options != "" { - return nil, NoStorageOptions - } + logger = logger.With("storage", "mysql") opts := []mysql.Option{ mysql.WithDSN(dsn), - mysql.WithLogger(logger.With("storage", "mysql")), + mysql.WithLogger(logger), + } + if options != "" { + for k, v := range splitOptions(options) { + switch k { + case "delete": + if v == "1" { + opts = append(opts, mysql.WithDeleteCommands()) + logger.Debug("msg", "deleting commands") + } else if v != "0" { + return nil, fmt.Errorf("invalid value for delete option: %q", v) + } + default: + return nil, fmt.Errorf("invalid option: %q", k) + } + } } return mysql.New(opts...) } + +func splitOptions(s string) map[string]string { + out := make(map[string]string) + opts := strings.Split(s, ",") + for _, opt := range opts { + optKAndV := strings.SplitN(opt, "=", 2) + if len(optKAndV) < 2 { + optKAndV = append(optKAndV, "") + } + out[optKAndV[0]] = optKAndV[1] + } + return out +} diff --git a/docs/operations-guide.md b/docs/operations-guide.md index d3d48da..c73f24c 100644 --- a/docs/operations-guide.md +++ b/docs/operations-guide.md @@ -44,7 +44,7 @@ The `-storage`, `-dsn`, & `-storage-options` flags together configure the storag * `-storage file` -Configures the `file` storage backend. This manages enrollment and command data within plain filesystem files and directories. It has zero dependencies and should run out of the box. The `-dsn` flag specifies the filesystem directory for the database. +Configures the `file` storage backend. This manages enrollment and command data within plain filesystem files and directories. It has zero dependencies and should run out of the box. The `-dsn` flag specifies the filesystem directory for the database. The `file` backend has no storage options. *Example:* `-storage file -dsn /path/to/my/db` @@ -56,6 +56,13 @@ Configures the MySQL storage backend. The `-dsn` flag should be in the [format t *Example:* `-storage mysql -dsn nanomdm:nanomdm/mymdmdb` +Options are specified as a comma-separated list of "key=value" pairs. The mysql backend supports these options: + +* `delete=1`, `delete=0` + * This option turns on the command and response deleter. When enabled (with `delete=1`) command responses, queued commands, and commands themeselves will be deleted from the database after enrollments have responded to a command. + +*Example:* `-storage mysql -dsn nanomdm:nanomdm/mymdmdb -storage-options delete=1` + #### multi-storage backend You can configure multiple storage backends to be used simultaneously. Specifying multiple sets of `-storage`, `-dsn`, & `-storage-options` flags will configure the "multi-storage" adapter. The flags must be specified in sets and are related to each other in the order they're specified: for example the first `-storage` flag corresponds to the first `-dsn` flag and so forth. diff --git a/storage/mysql/mysql.go b/storage/mysql/mysql.go index ad78de6..cec2ced 100644 --- a/storage/mysql/mysql.go +++ b/storage/mysql/mysql.go @@ -18,6 +18,7 @@ var ErrNoCert = errors.New("no certificate in MDM Request") type MySQLStorage struct { logger log.Logger db *sql.DB + rm bool } type config struct { @@ -25,6 +26,7 @@ type config struct { dsn string db *sql.DB logger log.Logger + rm bool } type Option func(*config) @@ -53,6 +55,12 @@ func WithDB(db *sql.DB) Option { } } +func WithDeleteCommands() Option { + return func(c *config) { + c.rm = true + } +} + func New(opts ...Option) (*MySQLStorage, error) { cfg := &config{logger: log.NopLogger, driver: "mysql"} for _, opt := range opts { @@ -68,7 +76,7 @@ func New(opts ...Option) (*MySQLStorage, error) { if err = cfg.db.Ping(); err != nil { return nil, err } - return &MySQLStorage{db: cfg.db, logger: cfg.logger}, nil + return &MySQLStorage{db: cfg.db, logger: cfg.logger, rm: cfg.rm}, nil } // nullEmptyString returns a NULL string if s is empty. diff --git a/storage/mysql/queue.go b/storage/mysql/queue.go index 4e556fc..84bca10 100644 --- a/storage/mysql/queue.go +++ b/storage/mysql/queue.go @@ -47,6 +47,56 @@ func (m *MySQLStorage) EnqueueCommand(ctx context.Context, ids []string, cmd *md return nil, tx.Commit() } +func (s *MySQLStorage) deleteCommand(ctx context.Context, tx *sql.Tx, id, uuid string) error { + // delete command result (i.e. NotNows) and this queued command + _, err := tx.ExecContext( + ctx, ` +DELETE + q, r +FROM + enrollment_queue AS q + LEFT JOIN command_results AS r + ON q.command_uuid = r.command_uuid AND r.id = q.id +WHERE + q.id = ? AND q.command_uuid = ?; +`, + id, uuid, + ) + if err != nil { + return err + } + // now delete the actual command assuming any remaining queued + // enrolmments have been deleted + _, err = tx.ExecContext( + ctx, ` +DELETE + c +FROM + commands AS c + LEFT JOIN enrollment_queue AS q + ON q.command_uuid = c.command_uuid +WHERE + c.command_uuid = ? AND q.id IS NULL; +`, + uuid, + ) + return err +} + +func (s *MySQLStorage) deleteCommandTx(r *mdm.Request, result *mdm.CommandResults) error { + tx, err := s.db.BeginTx(r.Context, nil) + if err != nil { + return err + } + if err = s.deleteCommand(r.Context, tx, r.ID, result.CommandUUID); err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + return fmt.Errorf("rollback error: %w; while trying to handle error: %v", rbErr, err) + } + return err + } + return tx.Commit() +} + func (s *MySQLStorage) StoreCommandReport(r *mdm.Request, result *mdm.CommandResults) error { if err := s.updateLastSeen(r); err != nil { return err @@ -54,6 +104,9 @@ func (s *MySQLStorage) StoreCommandReport(r *mdm.Request, result *mdm.CommandRes if result.Status == "Idle" { return nil } + if s.rm && (result.Status == "Acknowledged" || result.Status == "Error") { + return s.deleteCommandTx(r, result) + } notNowConstants := "NULL, 0" notNowBumpTallySQL := "" // note that due to the ON DUPLICATE KEY we don't UPDATE the diff --git a/storage/mysql/queue_test.go b/storage/mysql/queue_test.go index f14e9b3..49ee203 100644 --- a/storage/mysql/queue_test.go +++ b/storage/mysql/queue_test.go @@ -87,7 +87,7 @@ func TestQueue(t *testing.T) { t.Fatal("MySQL DSN flag not provided to test") } - storage, err := New(WithDSN(*flDSN)) + storage, err := New(WithDSN(*flDSN), WithDeleteCommands()) if err != nil { t.Fatal(err) } @@ -97,5 +97,16 @@ func TestQueue(t *testing.T) { t.Fatal(err) } - test.TestQueue(t, deviceUDID, storage) + t.Run("WithDeleteCommands()", func(t *testing.T) { + test.TestQueue(t, deviceUDID, storage) + }) + + storage, err = New(WithDSN(*flDSN)) + if err != nil { + t.Fatal(err) + } + + t.Run("normal", func(t *testing.T) { + test.TestQueue(t, deviceUDID, storage) + }) }