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.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/scout_agent/database/snapshots.rb', line 139

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.



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/scout_agent/database/snapshots.rb', line 107

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.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/scout_agent/database/snapshots.rb', line 185

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)


125
126
127
128
129
130
131
132
# File 'lib/scout_agent/database/snapshots.rb', line 125

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

#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.



59
60
61
62
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
# File 'lib/scout_agent/database/snapshots.rb', line 59

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