Class: ScoutAgent::Database::Snapshots

Inherits:
ScoutAgent::Database show all
Defined in:
lib/scout_agent/database/snapshots.rb

Overview

This database holds snapshot commands and results. These commands are used as a way of recording the current state of the box the agent runs on. The results of these commands may help identify causes of problems reported by missions the agent runs.

Constant Summary collapse

DEFAULT_TIMEOUT =

A maximum time in seconds after which a command run is halted.

60
DEFAULT_INTERVAL =

A minimum time in minutes that must pass before a command will be run again in the next snapshot.

10
RUNS_LIMIT =

A size limit for the runs table to prevent data from building up.

3000

Instance Attribute Summary

Attributes inherited from ScoutAgent::Database

#log

Instance Method Summary collapse

Methods inherited from ScoutAgent::Database

#initialize, load, #locked?, #maintain, #migrate, #path, path, #prepare_connection, #query, #read_from_sqlite, #read_locked?, #schema_version, #write_locked?, #write_to_sqlite

Constructor Details

This class inherits a constructor from ScoutAgent::Database

Instance Method Details

#complete_run(command, output, exit_status, snapshot_at, run_time) ⇒ Object

Marks command as just having run in the database and updates its next_run_at Time. A run is also created for the command documenting its output, exit_status, snapshot_at Time, and run_time.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/scout_agent/database/snapshots.rb', line 163

def complete_run(command, output, exit_status, snapshot_at, run_time)
  write_to_sqlite do |sqlite|
    # record run
    params = [ command[:code],
               output,
               exit_status,
               snapshot_at.to_db_s,
               run_time ]
    begin
      sqlite.execute(<<-END_INSERT_RUN.trim, *params)
      INSERT INTO
        runs(   code, output, exit_status, snapshot_at, run_time )
        VALUES(    ?,      ?,           ?,           ?,        ? )
      END_INSERT_RUN
    rescue Amalgalite::SQLite3::Error => error  # failed to add run
      # do nothing:  skip bad command and move on
      log.error( "Database bad command run (#{command[:code]}) error:  " +
                 "#{error.message}." )
    end
    # update command
    run_time = Time.now
    params   = [ run_time.to_db_s,
                 ( run_time +
                   command[:interval] * 60 ).to_db_s(:trim_seconds),
                 command[:id] ]
    begin
      sqlite.execute(<<-END_UPDATE_COMMAND.trim, *params)
      UPDATE commands SET last_run_at = ?, next_run_at = ? WHERE ROWID = ?
      END_UPDATE_COMMAND
    rescue Amalgalite::SQLite3::Error => error  # failed to update command
      # do nothing:  command will be run again
      log.error( "Database bad command (#{command[:code]}) update " +
                 "error:  #{error.message}." )
    end
  end
rescue Amalgalite::SQLite3::Error => error  # failed to get a write lock
  # try again to update commands later
  log.error("Database complete command locking error:  #{error.message}.")
end

#current_commandsObject

Returns all current commands (id, timeout, interval, last_run_at, and code) that should be executed as part of the current snapshot. An empty Array is returned if no commands are found.



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/scout_agent/database/snapshots.rb', line 111

def current_commands
  query(<<-END_FIND_COMMANDS.trim, Time.now.to_db_s) { |row|
  SELECT   ROWID AS id, timeout, interval, last_run_at, code
  FROM     commands
  WHERE    next_run_at <= ?
  END_FIND_COMMANDS
    row[:last_run_at] = Time.from_db_s(row[:last_run_at])
  }
rescue Amalgalite::SQLite3::Error => error  # failed to find commands
  log.error("Database commands error:  #{error.message}.")
  Array.new  # return empty results
end

#current_runsObject

This method returns command runs intended for the Scout server.

The process is very similar to how mission generated reports are pulled. See ScoutAgent::Database::MissionLog#current_reports() for details.



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/scout_agent/database/snapshots.rb', line 209

def current_runs
  write_to_sqlite { |sqlite|
    # read the current runs
    begin
      run_ids = Array.new
      runs    = query(<<-END_FIND_RUNS.trim) { |row|
      SELECT     ROWID AS id, code, output, exit_status,
                 snapshot_at AS created_at, run_time
      FROM       runs
      ORDER BY   snapshot_at
      LIMIT      500
      END_FIND_RUNS
        if created = Time.from_db_s(row[:created_at])
          row[:created_at] = created.utc.to_db_s
        else
          log.warn("Run timestamp missing.")
        end
        run_ids << row.delete_at(:id)
      }
    rescue Amalgalite::SQLite3::Error => error  # failed to find runs
      log.error("Database runs error:  #{error.message}.")
      return Array.new  # return empty results
    end
    return runs if runs.empty?
    # delete the runs we read
    begin
      sqlite.execute(<<-END_DELETE_RUNS.trim, *run_ids)
      DELETE FROM runs
      WHERE       ROWID IN (#{(['?'] * run_ids.size).join(', ')})
      END_DELETE_RUNS
    rescue Amalgalite::SQLite3::Error => error  # failed to remove runs
      # cancel sending this batch
      log.error("Database delivered runs error:  #{error.message}.")
      sqlite.rollback   # we can't submit unless we're sure they are gone
      return Array.new  # return empty results
    end
    runs  # the runs ready for sending
  }
rescue Amalgalite::SQLite3::Error => error  # failed to get a write lock
  # try again to read runs later
  log.error("Database runs locking error:  #{error.message}.")
end

#have_commands?Boolean

Returns true or false to indicate if any commands are stored in the snapshot database. If the database is inaccessible and the answer cannot be determined, nil is returned.

Returns:

  • (Boolean)


129
130
131
132
133
134
135
136
# File 'lib/scout_agent/database/snapshots.rb', line 129

def have_commands?
  read_from_sqlite { |sqlite|
    !!sqlite.first_value_from("SELECT ROWID FROM commands LIMIT 1")
  }
rescue Amalgalite::SQLite3::Error => error  # failed to find commands
  log.error("Database command check error:  #{error.message}.")
  nil  # commands not found
end

#reset_all_commandsObject

All commands are reset so they will be run again at the first available opportunity.



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/scout_agent/database/snapshots.rb', line 142

def reset_all_commands
  write_to_sqlite do |sqlite|
    sqlite.execute(<<-END_RESET_COMMANDS.trim)
    UPDATE commands
    SET    next_run_at = strftime('%Y-%m-%d %H:%M', 'now', 'localtime')
    END_RESET_COMMANDS
  end
rescue Amalgalite::SQLite3::Error => error  # failed to reset commands
  # do nothing:  commands will be run at their scheduled time
  log.error("Database command reset error:  #{error.message}.")
end

#update_commands(commands) ⇒ Object

Updates the list of snapshot commands in the database. Existing commands are updated, new commands are added, and commands no longer in the list are deleted.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/scout_agent/database/snapshots.rb', line 63

def update_commands(commands)
  write_to_sqlite do |sqlite|
    codes = commands.map { |m| m["code"] }
    begin
      sqlite.execute(<<-END_DELETE_COMMANDS.trim, *codes)
      DELETE FROM commands
      WHERE       code NOT IN (#{(['?'] * codes.size).join(', ')})
      END_DELETE_COMMANDS
    rescue Amalgalite::SQLite3::Error => error  # failed to remove
      log.error("Database command updating error:  #{error.message}.")
      return  # try again to update commands later
    end
    commands.each do |command|
      params = [ command["timeout"].to_s =~ /\A\d*[1-9]\z/ ?
                   command["timeout"].to_i                 :
                   DEFAULT_TIMEOUT,
                 (command["interval"] || DEFAULT_INTERVAL).to_i,
                 command["code"] ]
      begin
        if sqlite.first_value_from(
             "SELECT ROWID FROM commands WHERE code = ? LIMIT 1",
             command["code"]
           )
          sqlite.execute(<<-END_UPDATE_COMMAND.trim, *params)
          UPDATE commands SET timeout = ?, interval = ? WHERE code = ?
          END_UPDATE_COMMAND
        else
          sqlite.execute(<<-END_INSERT_COMMAND.trim, *params)
          INSERT INTO commands(timeout, interval, code) VALUES(?, ?, ?)
          END_INSERT_COMMAND
        end
      rescue Amalgalite::SQLite3::Error => error  # failed to set command
        # do nothing:  skip bad command and move on
        log.error( "Database bad command (#{command['code']}) error:  " +
                   "#{error.message}." )
      end
    end
  end
rescue Amalgalite::SQLite3::Error => error  # failed to get a write lock
  # try again to update commands later
  log.error("Database command update locking error:  #{error.message}.")
end

#update_schema(version = schema_version) ⇒ Object

Builds a schema for tables holding commands and the runs of those commands. The runs table is size controlled via a trigger to prevent infinite data growth.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/scout_agent/database/snapshots.rb', line 28

def update_schema(version = schema_version)
  case version
  when 0
    <<-END_INITIAL_SCHEMA.trim
    CREATE TABLE commands (
      code        REQUIRED_TEXT_TYPE    PRIMARY KEY,
      timeout     POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_TIMEOUT},
      interval    DEFAULT_INTEGER_TYPE  #{DEFAULT_INTERVAL},
      last_run_at DATETIME_TYPE,
      next_run_at DATETIME_TYPE
    );
    DEFAULT_LOCALTIME_TRIGGER commands next_run_at trim_seconds

    CREATE TABLE runs (
      code        REQUIRED_TEXT_TYPE,
      output      TEXT,
      exit_status INTEGER,
      snapshot_at REQUIRED_DATETIME_TYPE,
      run_time    ZERO_OR_POSITIVE_REAL_TYPE,
      PRIMARY KEY(code, snapshot_at)
    );
    LIMIT_TABLE_SIZE_TRIGGER runs #{RUNS_LIMIT}
    END_INITIAL_SCHEMA
  end
end