scout_agent 3.0.3 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ == 3.0.4
2
+
3
+ * Upgraded to Arrayfields 4.7.3 to silence a warning on Ruby 1.9
4
+ * Added HTTP proxy support
5
+ * Added replies to acknowledge received messages from XMPP when a message ID is
6
+ included in the command
7
+ * Turned XMPP on and aimed it at the real Scout server
8
+ * Greatly improved error handling for the XMPP process
9
+ * Added logging and status tracking for the XMPP operations
10
+
1
11
  == 3.0.3
2
12
 
3
13
  * Upgraded to JSON 1.1.4 for Ruby 1.9 compatibility
data/Rakefile CHANGED
@@ -34,7 +34,7 @@ SA_SPEC = Gem::Specification.new do |spec|
34
34
 
35
35
  spec.require_path = "lib"
36
36
 
37
- spec.add_dependency("arrayfields", "=4.7.2") # fix Amalgalite's results
37
+ spec.add_dependency("arrayfields", "=4.7.3") # fix Amalgalite's results
38
38
  spec.add_dependency("amalgalite", "=0.9.0")
39
39
  spec.add_dependency("rest-client", "=0.9.2")
40
40
  spec.add_dependency("json", "=1.1.4")
data/lib/scout_agent.rb CHANGED
@@ -65,7 +65,7 @@ module ScoutAgent
65
65
  end
66
66
 
67
67
  # The version of this agent.
68
- VERSION = "3.0.3".freeze
68
+ VERSION = "3.0.4".freeze
69
69
  # A Pathname reference to the agent code directory, used in dynamic loading.
70
70
  LIB_DIR = Pathname.new(File.dirname(__FILE__)) + agent_name
71
71
  end
@@ -8,6 +8,8 @@ require "scout_agent/order"
8
8
  module ScoutAgent
9
9
  class Agent
10
10
  class CommunicationAgent < Agent
11
+ RECONNECT_WAIT = 60
12
+
11
13
  def initialize
12
14
  super # setup our log and status
13
15
 
@@ -21,16 +23,13 @@ module ScoutAgent
21
23
  end
22
24
 
23
25
  def run
24
- if Plan.test_mode?
25
- login
26
- update_status("Online since #{Time.now.utc.to_db_s}")
27
- fetch_roster
28
- install_subscriptions_callback
29
- install_messages_callback
30
- listen
31
- else
32
- loop { sleep 60 }
33
- end
26
+ login
27
+ update_status("Online since #{Time.now.utc.to_db_s}")
28
+ fetch_roster
29
+ install_subscriptions_callback
30
+ install_messages_callback
31
+ listen
32
+ close_connection
34
33
  end
35
34
 
36
35
  def finish
@@ -46,40 +45,117 @@ module ScoutAgent
46
45
  def login
47
46
  Thread.abort_on_exception = true # make XMPP4R fail fast
48
47
  @agent_jid = Jabber::JID.new("#{agent_key}@#{jabber_server}/agent")
48
+ log.info("Connecting to XMPP: #{@agent_jid}")
49
49
  @jabber = Jabber::Client.new(@agent_jid)
50
- no_warnings { @jabber.connect }
51
- @jabber.auth(agent_key)
50
+ @jabber.on_exception do |error, stream, during|
51
+ try_connection
52
+ end
53
+ try_connection
54
+ end
55
+
56
+ def try_connection
57
+ until connect_and_authenticate?
58
+ log.info( "Waiting #{RECONNECT_WAIT} seconds before making another " +
59
+ "connection attempt." )
60
+ sleep RECONNECT_WAIT
61
+ end
62
+ end
63
+
64
+ def connect_and_authenticate?
65
+ status("Connecting")
66
+ close_connection
67
+ begin
68
+ no_warnings { @jabber.connect }
69
+ rescue Exception => error # connection failure
70
+ log.error("Failed to connect (#{error.class}: #{error.message}).")
71
+ return false
72
+ end
73
+ begin
74
+ @jabber.auth(agent_key)
75
+ rescue Jabber::ClientAuthenticationFailure # authentication failure
76
+ log.error("Authentication rejected.")
77
+ close_connection
78
+ return false
79
+ end
80
+ true
52
81
  end
53
82
 
54
83
  def update_status(message, status = nil)
84
+ status("Queuing status change")
55
85
  presence = Jabber::Presence.new
56
86
  presence.status = message
57
87
  presence.show = status
58
- @jabber.send(presence)
88
+ # xmpp4r requires sending in a different Thread from the callbacks
89
+ Thread.new do
90
+ log.info("Setting status: #{message}")
91
+ begin
92
+ @jabber.send(presence)
93
+ rescue Exception # failure to update status
94
+ # do nothing: server will still see us online
95
+ log.error("Unable to update status.")
96
+ end
97
+ end
59
98
  end
60
99
 
61
100
  def fetch_roster
101
+ status("Preparing connection")
62
102
  @roster = Jabber::Roster::Helper.new(@jabber)
63
103
  end
64
104
 
65
105
  def install_subscriptions_callback
66
106
  @roster.add_subscription_request_callback do |_, presence|
107
+ log.info("Accepting subscription: #{presence.from}")
67
108
  @roster.accept_subscription(presence.from)
68
109
  end
69
110
  end
70
111
 
71
112
  def install_messages_callback
72
113
  @jabber.add_message_callback do |message|
73
- if order = Order.can_handle?(message)
114
+ log.info("Received message from #{message.from}: #{message.body}")
115
+ status("Parsing command")
116
+ modified_body = nil
117
+ if message.body =~ /\A\s*\[\s*(\d+)\s*\]\s*(.*)/
118
+ modified_body = $2
119
+ send_chat_message(message.from, "received #{$1}")
120
+ end
121
+ if order = Order.can_handle?(message, modified_body)
122
+ status("Processing command")
74
123
  order.execute
124
+ else
125
+ log.warn("Unrecognized message format, ingoring.")
126
+ end
127
+ status("Listening for commands")
128
+ end
129
+ end
130
+
131
+ def send_chat_message(who, body)
132
+ status("Queuing message")
133
+ message = Jabber::Message.new(who, body)
134
+ message.type = :chat
135
+ # xmpp4r requires sending in a different Thread from the callbacks
136
+ Thread.new do
137
+ log.info("Sending message to #{who}: #{body}")
138
+ begin
139
+ @jabber.send(message)
140
+ rescue Exception # failure to send message
141
+ # do nothing: unable to reach the server
142
+ log.error("Unable to send message.")
75
143
  end
76
144
  end
77
145
  end
78
146
 
79
147
  def listen
148
+ log.info("Listening for commands.")
149
+ status("Listening for commands")
80
150
  @shutdown_thread = Thread.current
81
151
  Thread.stop
82
- @jabber.close
152
+ end
153
+
154
+ def close_connection
155
+ @jabber.close! if @jabber.is_connected?
156
+ rescue Exception # connection already closed
157
+ # do nothing: our connection is gone
158
+ log.warn("Failed to close connection.")
83
159
  end
84
160
 
85
161
  def agent_key
@@ -89,7 +165,8 @@ module ScoutAgent
89
165
  end
90
166
 
91
167
  def jabber_server
92
- @jabber_server ||= Plan.test_mode? ? "jabber.org" : "FIXME"
168
+ @jabber_server ||= Plan.test_mode? ? "jabber.org" :
169
+ URI.parse(Plan.server_url).host
93
170
  end
94
171
  end
95
172
  end
@@ -3,6 +3,12 @@
3
3
 
4
4
  module ScoutAgent
5
5
  class Database
6
+ #
7
+ # This database encapsulates the main function of the Scout agent: running
8
+ # missions. Details of the current plan and the missions of that plan are
9
+ # stored in these tables. As missions are executed, they build up reports
10
+ # which are also held here until they can be pushed to the Scout server.
11
+ #
6
12
  class MissionLog < Database
7
13
  #
8
14
  # A default number of seconds a mission is allowed to run before it is
@@ -14,6 +20,10 @@ module ScoutAgent
14
20
  # A size limit for the reports table to prevent data from building up.
15
21
  REPORTS_LIMIT = 3000
16
22
 
23
+ #
24
+ # Build a schema for storing plans, missions, and reports. The reports
25
+ # table is size controlled by trigger to prevent infinite data growth.
26
+ #
17
27
  def update_schema(version = schema_version)
18
28
  case version
19
29
  when 0
@@ -50,6 +60,14 @@ module ScoutAgent
50
60
  end
51
61
  end
52
62
 
63
+ ################
64
+ ### The Plan ###
65
+ ################
66
+
67
+ #
68
+ # Returns the last known plan (+id+ and +last_modified+ date) or +nil+
69
+ # if none exists.
70
+ #
53
71
  def current_plan
54
72
  plan = read_from_sqlite { |sqlite|
55
73
  sqlite.first_row_from(<<-END_FIND_PLAN.trim)
@@ -65,6 +83,13 @@ module ScoutAgent
65
83
  nil # not found
66
84
  end
67
85
 
86
+ #
87
+ # Given a new +last_modified+ date (as a String) and an Array of
88
+ # +missions+, this method attemps and all-or-nothing update of the current
89
+ # plan and missions. The plan and any missions that were already present
90
+ # are simply updated. New missions are added and missions no longer in
91
+ # the list are removed. New missions receive a +next_run_at+ Time of now.
92
+ #
68
93
  def update_plan(last_modified, missions)
69
94
  write_to_sqlite do |sqlite|
70
95
  begin
@@ -127,6 +152,16 @@ module ScoutAgent
127
152
  log.error("Database mission update locking error: #{error.message}.")
128
153
  end
129
154
 
155
+ ################
156
+ ### Missions ###
157
+ ################
158
+
159
+ #
160
+ # Returns the current mission (+id+, +timeout+, +interval+, +last_run_at+,
161
+ # +name+, +code+, +options+, and +memory+) that should be run. The
162
+ # +options+ and +memory+ fields are JSON parsed when possible. If there
163
+ # are no missions scheduled to run at this time, +nil+ is returned.
164
+ #
130
165
  def current_mission
131
166
  mission = read_from_sqlite { |sqlite|
132
167
  return nil unless plan = current_plan
@@ -159,6 +194,10 @@ module ScoutAgent
159
194
  nil # not found
160
195
  end
161
196
 
197
+ #
198
+ # Given a +mission_id+ and a +memory+ Hash, this method updates a
199
+ # mission's stored memory.
200
+ #
162
201
  def update_mission_memory(mission_id, memory)
163
202
  write_to_sqlite do |sqlite|
164
203
  sqlite.execute(<<-END_UPDATE_MISSION.trim, memory.to_json, mission_id)
@@ -170,6 +209,10 @@ module ScoutAgent
170
209
  log.error("Database memory update error: #{error.message}.")
171
210
  end
172
211
 
212
+ #
213
+ # Marks +mission+ as complete in the database by recording its
214
+ # +last_run_at+ Time and setting a +next_run_at+ Time.
215
+ #
173
216
  def complete_mission(mission)
174
217
  write_to_sqlite do |sqlite|
175
218
  run_time = Time.now
@@ -187,7 +230,12 @@ module ScoutAgent
187
230
  false # warn the caller that the mission will still match
188
231
  end
189
232
 
233
+ #
234
+ # All passed mission +ids+ are reset so they will be run again at the
235
+ # first available opportunity.
236
+ #
190
237
  def reset_missions(*ids)
238
+ return if ids.empty?
191
239
  write_to_sqlite do |sqlite|
192
240
  sqlite.execute(<<-END_RESET_MISSIONS.trim, *ids)
193
241
  UPDATE missions
@@ -200,6 +248,38 @@ module ScoutAgent
200
248
  log.error("Database mission reset error: #{error.message}.")
201
249
  end
202
250
 
251
+ #
252
+ # Returns the number of seconds until another mission will be ready for
253
+ # running. If the count would be zero or less seconds, the
254
+ # +DEFAULT_INTERVAL+ is returned (in seconds) to prevent the agent from
255
+ # entering a busy loop.
256
+ #
257
+ def seconds_to_next_mission
258
+ default = DEFAULT_INTERVAL * 60
259
+ next_run_at = read_from_sqlite { |sqlite|
260
+ sqlite.first_value_from(<<-END_FIND_MISSION.trim)
261
+ SELECT next_run_at FROM missions ORDER BY next_run_at LIMIT 1
262
+ END_FIND_MISSION
263
+ }
264
+ if next_run = Time.from_db_s(next_run_at)
265
+ seconds = next_run - Time.now
266
+ seconds > 0 ? seconds : default
267
+ else
268
+ default
269
+ end
270
+ rescue Amalgalite::SQLite3::Error => error # failed to locate last run
271
+ log.error("Database next mission error: #{error.message}.")
272
+ default # use default
273
+ end
274
+
275
+ ###############
276
+ ### Reports ###
277
+ ###############
278
+
279
+ #
280
+ # Adds a report for +mission_id+ of +type+ with +fields+ to the database.
281
+ # Returns +true+ if the write succeeded, or +false+ if it did not.
282
+ #
203
283
  def write_report(mission_id, type, fields)
204
284
  write_to_sqlite do |sqlite|
205
285
  params = [mission_id, type, fields.to_json]
@@ -213,8 +293,24 @@ module ScoutAgent
213
293
  false # couldn't be written
214
294
  end
215
295
 
296
+ #
297
+ # This method returns an Array of all reports (+type+, +fields+,
298
+ # +created_at+, and +plugin_id+) that should be submitted to the Scout
299
+ # server. The report +fields+ will be JSON parsed when possible and
300
+ # +created_at+ is converted to a proper Time object.
301
+ #
302
+ # The act of reading these reports also triggers their removal from the
303
+ # database so we avoid sending duplicates to the server. This does mean
304
+ # that we lose data if anything goes wrong in the sending process. This
305
+ # is considered an acceptable risk, because even a
306
+ # delete-after-a-successful-send stragety is subject to duplication
307
+ # (the request might timeout but eventually complete on the server,
308
+ # for example). If anything goes wrong with the reading or deletion,
309
+ # the entire process is canceled and an empty Array is returned.
310
+ #
216
311
  def current_reports
217
312
  write_to_sqlite { |sqlite|
313
+ # read the current reports
218
314
  begin
219
315
  report_ids = Array.new
220
316
  reports = query(<<-END_FIND_REPORTS.trim) { |row|
@@ -243,6 +339,7 @@ module ScoutAgent
243
339
  return Array.new # return empty results
244
340
  end
245
341
  return reports if reports.empty?
342
+ # delete the reports we read
246
343
  begin
247
344
  sqlite.execute(<<-END_DELETE_REPORTS.trim, *report_ids)
248
345
  DELETE FROM reports
@@ -260,24 +357,6 @@ module ScoutAgent
260
357
  # try again to read reports later
261
358
  log.error("Database reports locking error: #{error.message}.")
262
359
  end
263
-
264
- def seconds_to_next_mission
265
- default = DEFAULT_INTERVAL * 60
266
- next_run_at = read_from_sqlite { |sqlite|
267
- sqlite.first_value_from(<<-END_FIND_MISSION.trim)
268
- SELECT next_run_at FROM missions ORDER BY next_run_at LIMIT 1
269
- END_FIND_MISSION
270
- }
271
- if next_run = Time.from_db_s(next_run_at)
272
- seconds = next_run - Time.now
273
- seconds > 0 ? seconds : default
274
- else
275
- default
276
- end
277
- rescue Amalgalite::SQLite3::Error => error # failed to locate last run
278
- log.error("Database next mission error: #{error.message}.")
279
- default # use default
280
- end
281
360
  end
282
361
  end
283
362
  end
@@ -3,10 +3,19 @@
3
3
 
4
4
  module ScoutAgent
5
5
  class Database
6
+ #
7
+ # This database is used to stored messages queued from external processes.
8
+ # Such messages can be data for missions or full reports ready for
9
+ # submission to the Scout server.
10
+ #
6
11
  class Queue < Database
7
12
  # A size limit for the queue to prevent data from building up.
8
13
  QUEUE_LIMIT = 3000
9
14
 
15
+ #
16
+ # Builds a schema for the queue table. This table is size controlled by a
17
+ # trigger to prevent infinite data growth.
18
+ #
10
19
  def update_schema(version = schema_version)
11
20
  case version
12
21
  when 0
@@ -24,6 +33,14 @@ module ScoutAgent
24
33
  end
25
34
  end
26
35
 
36
+ #
37
+ # Adds a message to the queue. The passed +mission_id+ needs to be an
38
+ # Integer ID for a mission or one of the Strings <tt>'report'</tt>,
39
+ # <tt>'hint'</tt>, <tt>'alert'</tt>, or <tt>'error'</tt> for a full
40
+ # report. The +fields+ parameter is expected to be a Hash of fields and
41
+ # should include a <tt>'plugin_id'</tt> key identifying what the data is
42
+ # for when the message is a report for the server.
43
+ #
27
44
  def enqueue(mission_id, fields)
28
45
  write_to_sqlite do |sqlite|
29
46
  sqlite.execute(<<-END_ENQUEUE.trim, mission_id, fields.to_json)
@@ -36,6 +53,12 @@ module ScoutAgent
36
53
  false # reject bad message
37
54
  end
38
55
 
56
+ #
57
+ # Returns a message (+id+, +fields+, and +created_at+) queued for
58
+ # +mission_id+. The +fields+ are JSON parsed if possible and +created_at+
59
+ # is converted to a Time object. This method will return +nil+ if no
60
+ # messages are queued for +mission_id+.
61
+ #
39
62
  def peek(mission_id)
40
63
  queued = read_from_sqlite { |sqlite|
41
64
  sqlite.first_row_from(<<-END_FIND_QUEUED.trim, mission_id.to_s)
@@ -63,8 +86,16 @@ module ScoutAgent
63
86
  nil # not found
64
87
  end
65
88
 
89
+ #
90
+ # This method returns queued reports intended for the Scout server.
91
+ #
92
+ # The process is pretty much identical to how mission generated reports
93
+ # are pulled. See ScoutAgent::Database::MissionLog#current_reports() for
94
+ # details.
95
+ #
66
96
  def queued_reports
67
97
  write_to_sqlite { |sqlite|
98
+ # read the current reports
68
99
  begin
69
100
  report_ids = Array.new
70
101
  reports = query(<<-END_FIND_REPORTS.trim) { |row|
@@ -95,6 +126,7 @@ module ScoutAgent
95
126
  return Array.new # return empty results
96
127
  end
97
128
  return reports if reports.empty?
129
+ # delete the reports we read
98
130
  unless dequeue(*report_ids)
99
131
  # cancel sending this batch
100
132
  sqlite.rollback # we can't submit unless we're sure they are gone
@@ -107,7 +139,12 @@ module ScoutAgent
107
139
  log.error("Database queued reports locking error: #{error.message}.")
108
140
  end
109
141
 
142
+ #
143
+ # Removes queued messages from the database by their +ids+. Returns
144
+ # +true+ if the removal succeeded, or +false+ otherwise.
145
+ #
110
146
  def dequeue(*ids)
147
+ return true if ids.empty?
111
148
  write_to_sqlite do |sqlite|
112
149
  sqlite.execute(<<-END_DELETE_QUEUED.trim, *ids)
113
150
  DELETE FROM queue WHERE ROWID IN (#{(['?'] * ids.size).join(', ')})
@@ -3,6 +3,12 @@
3
3
 
4
4
  module ScoutAgent
5
5
  class Database
6
+ #
7
+ # This database holds snapshot commands and results. These commands are
8
+ # used as a way of recording the current state of the box the agent runs on.
9
+ # The results of these commands may help identify causes of problems
10
+ # reported by missions the agent runs.
11
+ #
6
12
  class Snapshots < Database
7
13
  # A maximum time in seconds after which a command run is halted.
8
14
  DEFAULT_TIMEOUT = 60
@@ -14,6 +20,11 @@ module ScoutAgent
14
20
  # A size limit for the runs table to prevent data from building up.
15
21
  RUNS_LIMIT = 3000
16
22
 
23
+ #
24
+ # Builds a schema for tables holding commands and the runs of those
25
+ # commands. The runs table is size controlled via a trigger to prevent
26
+ # infinite data growth.
27
+ #
17
28
  def update_schema(version = schema_version)
18
29
  case version
19
30
  when 0
@@ -39,7 +50,12 @@ module ScoutAgent
39
50
  END_INITIAL_SCHEMA
40
51
  end
41
52
  end
42
-
53
+
54
+ #
55
+ # Updates the list of snapshot +commands+ in the database. Existing
56
+ # commands are updated, new commands are added, and commands no longer in
57
+ # the list are deleted.
58
+ #
43
59
  def update_commands(commands)
44
60
  write_to_sqlite do |sqlite|
45
61
  codes = commands.map { |m| m["code"] }
@@ -83,6 +99,11 @@ module ScoutAgent
83
99
  log.error("Database command update locking error: #{error.message}.")
84
100
  end
85
101
 
102
+ #
103
+ # Returns all current commands (+id+, +timeout+, +interval+,
104
+ # +last_run_at+, and +code+) that should be executed as part of the
105
+ # current snapshot. An empty Array is returned if no commands are found.
106
+ #
86
107
  def current_commands
87
108
  query(<<-END_FIND_COMMANDS.trim, Time.now.to_db_s) { |row|
88
109
  SELECT ROWID AS id, timeout, interval, last_run_at, code
@@ -96,6 +117,11 @@ module ScoutAgent
96
117
  Array.new # return empty results
97
118
  end
98
119
 
120
+ #
121
+ # Returns +true+ or +false+ to indicate if any commands are stored in the
122
+ # snapshot database. If the database is inaccessible and the answer
123
+ # cannot be determined, +nil+ is returned.
124
+ #
99
125
  def have_commands?
100
126
  read_from_sqlite { |sqlite|
101
127
  !!sqlite.first_value_from("SELECT ROWID FROM commands LIMIT 1")
@@ -105,8 +131,14 @@ module ScoutAgent
105
131
  nil # commands not found
106
132
  end
107
133
 
134
+ #
135
+ # Marks +command+ as just having run in the database and updates its
136
+ # +next_run_at+ Time. A run is also created for the +command+ documenting
137
+ # its +output+, +exit_status+, +snapshot_at+ Time, and +run_time+.
138
+ #
108
139
  def complete_run(command, output, exit_status, snapshot_at, run_time)
109
140
  write_to_sqlite do |sqlite|
141
+ # record run
110
142
  params = [ command[:code],
111
143
  output,
112
144
  exit_status,
@@ -123,6 +155,7 @@ module ScoutAgent
123
155
  log.error( "Database bad command run (#{command[:code]}) error: " +
124
156
  "#{error.message}." )
125
157
  end
158
+ # update command
126
159
  run_time = Time.now
127
160
  params = [ run_time.to_db_s,
128
161
  ( run_time +
@@ -143,8 +176,15 @@ module ScoutAgent
143
176
  log.error("Database complete command locking error: #{error.message}.")
144
177
  end
145
178
 
179
+ #
180
+ # This method returns command runs intended for the Scout server.
181
+ #
182
+ # The process is very similar to how mission generated reports are pulled.
183
+ # See ScoutAgent::Database::MissionLog#current_reports() for details.
184
+ #
146
185
  def current_runs
147
186
  write_to_sqlite { |sqlite|
187
+ # read the current runs
148
188
  begin
149
189
  run_ids = Array.new
150
190
  runs = query(<<-END_FIND_RUNS.trim) { |row|
@@ -166,6 +206,7 @@ module ScoutAgent
166
206
  return Array.new # return empty results
167
207
  end
168
208
  return runs if runs.empty?
209
+ # delete the runs we read
169
210
  begin
170
211
  sqlite.execute(<<-END_DELETE_RUNS.trim, *run_ids)
171
212
  DELETE FROM runs
@@ -3,7 +3,13 @@
3
3
 
4
4
  module ScoutAgent
5
5
  class Database
6
+ #
7
+ # This small database keeps track of the current status of all running
8
+ # processes withing the agent. This allows tracking of process function
9
+ # (represented as the process name), ID, and current task.
10
+ #
6
11
  class Statuses < Database
12
+ # Builds a schema for the process statuses table.
7
13
  def update_schema(version = schema_version)
8
14
  case version
9
15
  when 0
@@ -21,6 +27,11 @@ module ScoutAgent
21
27
  end
22
28
  end
23
29
 
30
+ #
31
+ # Record the current +status+ for the calling process. The process ID is
32
+ # determined with <tt>Process::pid()</tt> and +name+ is pulled from
33
+ # <tt>IDCard::me()#process_name()</tt> when available.
34
+ #
24
35
  def update_status(status, name = IDCard.me && IDCard.me.process_name)
25
36
  write_to_sqlite do |sqlite|
26
37
  sqlite.execute(<<-END_UPDATE_STATUS.trim, name, Process.pid, status)
@@ -33,6 +44,11 @@ module ScoutAgent
33
44
  log.error("Database status update error: #{error.message}.")
34
45
  end
35
46
 
47
+ #
48
+ # Removes the status record for +name+ (pulled from
49
+ # <tt>IDCard::me()#process_name()</tt> by default). This is generally
50
+ # done by processes in an <tt>at_exit()</tt> block.
51
+ #
36
52
  def clear_status(name = IDCard.me && IDCard.me.process_name)
37
53
  write_to_sqlite do |sqlite|
38
54
  sqlite.execute("DELETE FROM statuses WHERE name = ?", name)
@@ -42,6 +58,11 @@ module ScoutAgent
42
58
  log.error("Database status clearing error: #{error.message}.")
43
59
  end
44
60
 
61
+ #
62
+ # Returns the current statuses (+name+, +pid+, +status+,
63
+ # and +last_updated_at+) for all known processes. An empty Array is
64
+ # returned if no processes are currently active.
65
+ #
45
66
  def current_statuses
46
67
  query(<<-END_FIND_STATUSES.trim)
47
68
  SELECT name, pid, status, last_updated_at FROM statuses ORDER BY ROWID
@@ -51,6 +72,10 @@ module ScoutAgent
51
72
  Array.new # return empty results
52
73
  end
53
74
 
75
+ #
76
+ # Returns the current +status+ message for +name+ (pulled from
77
+ # <tt>IDCard::me()#process_name()</tt> by default).
78
+ #
54
79
  def current_status(name = IDCard.me && IDCard.me.process_name)
55
80
  read_from_sqlite { |sqlite|
56
81
  sqlite.first_value_from(<<-END_FIND_STATUS, name)
@@ -32,6 +32,10 @@ module ScoutAgent
32
32
  "The URL for the server to report to." ) do |url|
33
33
  switches[:server_url] = url
34
34
  end
35
+ opts.on( "-p", "--proxy URL", String,
36
+ "A proxy URL to pass HTTP requests through." ) do |url|
37
+ switches[:proxy_url] = url
38
+ end
35
39
  opts.on( "-d", "--[no-]daemon",
36
40
  "Run in the background as a daemon." ) do |boolean|
37
41
  switches[:run_as_daemon] = boolean
@@ -41,16 +41,17 @@ module ScoutAgent
41
41
  Plan.pid_dir + "#{@process_name}.pid"
42
42
  end
43
43
 
44
- # Returns the PID for the named process, or +nil+ if it cannot be read.
44
+ # Returns the PID for the named process or +nil+ if it cannot be read.
45
45
  def pid
46
46
  pid_file.read.to_i
47
- rescue Exception
47
+ rescue Exception # cannot read file
48
48
  nil
49
49
  end
50
50
 
51
51
  #
52
52
  # Tries to send +message+ as a signal to the process represented by this
53
- # instance. You can pass any message Process.kill() would understand.
53
+ # instance. You can pass any message <tt>Process.kill()</tt> would
54
+ # understand.
54
55
  #
55
56
  # Returns +true+ if the signal was sent, or +false+ if the PID file could
56
57
  # not be read. Any Exception raised during the send, such as Errno::ESRCH
@@ -71,9 +72,9 @@ module ScoutAgent
71
72
  # stale claims are ignored and replaced, if possible.
72
73
  #
73
74
  # This method returns +true+ in the claim succeeded and +false+ if it could
74
- # not happen for any reason. A return of +true+ indicates that me() has
75
- # been updated and an exit handle has been installed to revoke() this claim
76
- # as the process ends.
75
+ # not happen for any reason. A return of +true+ indicates that
76
+ # <tt>IDCard::me()</tt> has been updated and an exit handle has been
77
+ # installed to revoke() this claim as the process ends.
77
78
  #
78
79
  def authorize
79
80
  File.open(pid_file, File::CREAT | File::EXCL | File::WRONLY) do |pid|
@@ -91,9 +92,7 @@ module ScoutAgent
91
92
  self.class.me = self
92
93
 
93
94
  at_my_exit do
94
- unless revoke
95
- # log.error "Unable to unlink pid file: #{$!.message}" if log
96
- end
95
+ revoke
97
96
  end
98
97
  true
99
98
  rescue Errno::EEXIST # pid_file already exists
@@ -101,32 +100,24 @@ module ScoutAgent
101
100
  if pid.flock(File::LOCK_EX | File::LOCK_NB)
102
101
  if pid.read =~ /\A\d+/
103
102
  begin
104
- unless signal(0)
105
- # log.warn "Could not create or read PID file. " +
106
- # "You may need to the path to the config directory. " +
107
- # "See: http://scoutapp.com/help#data_file" if log
108
- end
103
+ signal(0) # check for the existing process
109
104
  rescue Errno::ESRCH # no such process
110
- # log.info "Stale PID file found. Clearing it and reloading..." if log
105
+ # stale PID file found, clearing it and reloading
111
106
  if revoke
112
107
  pid.flock(File::LOCK_UN) # release the lock before we recurse
113
108
  return authorize # try again
114
- else
115
- # log.info "Failed to clear PID." if log
116
109
  end
117
110
  rescue Errno::EACCES # don't have permission
118
111
  # nothing we can do so give up
119
112
  end
120
- else
121
- # nothing we can do so give up
122
113
  end
123
114
  pid.flock(File::LOCK_UN) # release the lock
124
115
  else
125
- # log.info "Couldn't grab a file lock to verify existing PID file." if log
116
+ # couldn't grab a file lock to verify existing PID file
126
117
  return false
127
118
  end
128
119
  end
129
- # log.warn "Process #{pid} was already running" if log
120
+ # process was already running
130
121
  false
131
122
  end
132
123
 
@@ -28,16 +28,22 @@ module ScoutAgent
28
28
  subclasses
29
29
  end
30
30
 
31
- def self.can_handle?(message)
31
+ def self.can_handle?(message, modified_body = nil)
32
32
  subclasses.each do |order|
33
- if match_details = order.match?(message)
33
+ if match_details = order.match?(message, modified_body)
34
34
  return order.new(message, match_details)
35
35
  end
36
36
  end
37
+ nil
37
38
  end
38
39
 
39
- def self.match?(message)
40
- const_defined?(:MATCH_RE) and const_get(:MATCH_RE).match(message.body)
40
+ def self.match?(message, modified_body = nil)
41
+ const_defined?(:MATCH_RE) and
42
+ const_get(:MATCH_RE).match(modified_body || message.body)
43
+ end
44
+
45
+ def self.master_agent
46
+ @master_agent ||= IDCard.new(:master)
41
47
  end
42
48
 
43
49
  def initialize(message, match_details)
@@ -55,5 +61,12 @@ module ScoutAgent
55
61
  raise NotImplementedError,
56
62
  "Subclasses must override ScoutAgent::Order#execute()."
57
63
  end
64
+
65
+ def notify_master
66
+ self.class.master_agent.signal("ALRM")
67
+ rescue Exception # unable to signal process
68
+ # do nothing: process will catch up when it restarts
69
+ log.warn("Unable to signal master process.")
70
+ end
58
71
  end
59
72
  end
@@ -15,19 +15,23 @@ module ScoutAgent
15
15
  @mission_log = db
16
16
  end
17
17
 
18
- def self.master_agent
19
- @master_agent ||= IDCard.new(:master)
20
- end
21
-
22
18
  def execute
23
19
  if id_str = match_details.captures.first
24
20
  ids = id_str.scan(/\d+/).map { |n| n.to_i }
25
21
  unless ids.empty?
26
- self.class.mission_log.reset_missions(ids)
22
+ s = ids.size == 1 ? "" : "s"
23
+ ids_str = case ids.size
24
+ when 1 then ids.first.to_s
25
+ when 2 then ids.join(" and ")
26
+ else ids[0..-2].join(", ") + ", and #{ids.last}"
27
+ end
28
+ log.info("Clearing wait time#{s} for mission#{s} (#{ids_str}).")
29
+ self.class.mission_log.reset_missions(*ids)
27
30
  end
28
31
  end
29
- self.class.master_agent.signal("ALRM")
32
+ log.info("Requesting an immediate check-in.")
33
+ notify_master
30
34
  end
31
35
  end
32
36
  end
33
- end
37
+ end
@@ -4,30 +4,12 @@
4
4
  module ScoutAgent
5
5
  class Order
6
6
  class SnapshotOrder < Order
7
- MATCH_RE = /\A\s*snap[-_]?shot(?:\s+(.+?))?\s*\z/
8
-
9
- def self.snapshots
10
- return @snapshots if defined? @snapshots
11
- unless db = Database.load(:snapshots, log)
12
- log.fatal("Could not load snapshots database.")
13
- exit
14
- end
15
- @snapshots = db
16
- end
17
-
18
- def self.master_agent
19
- @master_agent ||= IDCard.new(:master)
20
- end
7
+ MATCH_RE = /\A\s*snap[-_]?shot\s*\z/
21
8
 
22
9
  def execute
23
- # if id_str = match_details.captures.first
24
- # ids = id_str.scan(/\d+/).map { |n| n.to_i }
25
- # unless ids.empty?
26
- # self.class.mission_log.reset_missions(ids)
27
- # end
28
- # end
10
+ log.info("Requesting that a snapshot be taken.")
29
11
  API.take_snapshot
30
- self.class.master_agent.signal("ALRM")
12
+ notify_master
31
13
  end
32
14
  end
33
15
  end
@@ -18,6 +18,7 @@ module ScoutAgent
18
18
  # The default configuration for this agent.
19
19
  def defaults
20
20
  [ [:server_url, "http://beta.scoutapp.com:3000"],
21
+ [:proxy_url, nil],
21
22
  [:run_as_daemon, true],
22
23
  [:logging_level, "INFO"],
23
24
  [:test_mode, false],
@@ -37,7 +38,9 @@ module ScoutAgent
37
38
 
38
39
  # This method is used to set or reset the default configuration.
39
40
  def set_defaults
40
- defaults.each { |name, value| send("#{name}=", value) }
41
+ defaults.each do |name, value|
42
+ send("#{name}=", value)
43
+ end
41
44
  end
42
45
  alias_method :reset_defaults, :set_defaults
43
46
 
@@ -74,6 +77,7 @@ module ScoutAgent
74
77
  config_file.open(File::CREAT|File::EXCL|File::WRONLY) do |pid|
75
78
  pid.puts <<-END_DEFAULT_CONFIG.trim
76
79
  #!/usr/bin/env ruby -wKU
80
+ # encoding: UTF-8
77
81
 
78
82
  #
79
83
  # This file configures #{ScoutAgent.proper_agent_name}. The settings in
@@ -92,6 +96,12 @@ module ScoutAgent
92
96
 
93
97
  # The following is the URL used to reach the server:
94
98
  config.server_url = #{server_url.inspect}
99
+ #
100
+ # Set the following if your HTTP requests need to go through a proxy.
101
+ # All non-XMPP requests between the agent and the server will go through
102
+ # this URL.
103
+ #
104
+ config.proxy_url = #{proxy_url.inspect}
95
105
 
96
106
  #
97
107
  # When the following is true, the agent will disconnect from the
@@ -139,8 +149,8 @@ module ScoutAgent
139
149
  # Note: to have the prefix effect the location of this file or
140
150
  # to set your OS's configuration path, you will need to use
141
151
  # command-line switches (--prefix and --os-config-path respectively).
142
- # It wouldn't help to have those settings in this file, for obvious
143
- # reasons.
152
+ # It wouldn't help to have those settings in this file as they are
153
+ # needed before this file is loaded.
144
154
  #
145
155
  config.prefix_path = #{prefix_path.to_s.inspect}
146
156
 
@@ -161,11 +171,11 @@ module ScoutAgent
161
171
  # try to switch to. Choices are tried from left to right, so by
162
172
  # default the "daemon" user is used but the agent will try
163
173
  # "nobody" if daemon is not available. These are standard users
164
- # and groups for processes like this that run on Unix.
174
+ # and groups for long running processes on Unix.
165
175
  #
166
176
  # If you wish to improve security even more, we recommend creating
167
177
  # a user and group called "#{ScoutAgent.agent_name}" and replacing these
168
- # defaults with what you created. This puts you full control
178
+ # defaults with what you created. This puts you in full control
169
179
  # of what the agent will have access to on your system and makes
170
180
  # it easier to track what the agent is doing.
171
181
  #
@@ -193,7 +203,9 @@ module ScoutAgent
193
203
 
194
204
  # This method allows mass update through a +hash+ of settings.
195
205
  def update_from_switches(hash)
196
- hash.each { |name, value| send("#{name}=", value) }
206
+ hash.each do |name, value|
207
+ send("#{name}=", value)
208
+ end
197
209
  end
198
210
  alias_method :update_from_hash, :update_from_switches
199
211
 
@@ -211,9 +223,9 @@ module ScoutAgent
211
223
  # These paths prefix all of the specific application paths.
212
224
  #
213
225
  %w[os_config_path os_db_path os_pid_path os_log_path].each do |path|
214
- define_method(path) do
226
+ define_method(path) {
215
227
  prefix_path + super()
216
- end
228
+ }
217
229
  end
218
230
 
219
231
  #
@@ -230,9 +242,9 @@ module ScoutAgent
230
242
  # are named after the agent.
231
243
  #
232
244
  %w[db pid log].each do |path|
233
- define_method("#{path}_dir") do
245
+ define_method("#{path}_dir") {
234
246
  send("os_#{path}_path") + (super() || ScoutAgent.agent_name)
235
- end
247
+ }
236
248
  end
237
249
 
238
250
  #
@@ -243,7 +255,7 @@ module ScoutAgent
243
255
  { :db_dir => 0777,
244
256
  :pid_dir => 0775,
245
257
  :log_dir => 0777 }.each do |path, permissions|
246
- define_method("build_#{path}") do |group_id|
258
+ define_method("build_#{path}") { |group_id|
247
259
  begin
248
260
  send(path).mkpath
249
261
  send(path).chown(nil, group_id)
@@ -252,13 +264,17 @@ module ScoutAgent
252
264
  rescue Errno::EACCES, Errno::EPERM # don't have permission
253
265
  false
254
266
  end
255
- end
267
+ }
256
268
  end
257
269
 
258
270
  #############
259
271
  ### URL's ###
260
272
  #############
261
273
 
274
+ #
275
+ # Returns the full URL for check-ins by this agent. This is the
276
+ # +server_url+ plus the agent path and the key specific to this agent.
277
+ #
262
278
  def agent_url
263
279
  URI.join(server_url, "clients/#{agent_key}")
264
280
  end
@@ -267,6 +283,12 @@ module ScoutAgent
267
283
  ### Databases ###
268
284
  #################
269
285
 
286
+ #
287
+ # This method is used to prepare a database that can be accessed by any
288
+ # process. The database is first loaded by +name+. Then the permissions
289
+ # on that database are changed to allow all processes to read and write to
290
+ # the created database.
291
+ #
270
292
  def prepare_global_database(name)
271
293
  db = Database.load(name) or return false
272
294
  db.path.chmod(0777)
@@ -18,6 +18,8 @@ module ScoutAgent
18
18
  :headers => { :client_version => ScoutAgent::VERSION,
19
19
  :accept_encoding => "gzip" }
20
20
  )
21
+ # make sure proxy is set, if needed
22
+ RestClient.proxy = Plan.proxy_url
21
23
  end
22
24
 
23
25
  # The log connection notes will be written to.
@@ -40,13 +42,8 @@ module ScoutAgent
40
42
  log.warn("Plan was malformed zipped data.")
41
43
  "" # replace bad plan with empty plan
42
44
  rescue RestClient::RequestFailed => error # RestClient bug workaround
43
- # all success codes are OK, but other codes are not
44
- if error.http_code.between? 200, 299
45
- error.response.body # the returned plan
46
- else
47
- log.warn("Plan was returned as a failure code: #{error.http_code}.")
48
- nil # failed to retrieve plan
49
- end
45
+ log.warn("Plan was returned as a failure code: #{error.http_code}.")
46
+ nil # failed to retrieve plan
50
47
  rescue RestClient::NotModified
51
48
  log.info("Plan was not modified.")
52
49
  "" # empty plan
@@ -61,7 +58,7 @@ module ScoutAgent
61
58
  # as much as possible.
62
59
  #
63
60
  # If the ckeck-in succeeds, +true+ is returned. However, +false+ doesn't
64
- # ensure that we failed. RestClient may time out a long connection attempt,
61
+ # ensure that we failed. RestClient may timeout a long connection attempt,
65
62
  # but the server may still complete it eventually.
66
63
  #
67
64
  def post_checkin(data)
@@ -38,7 +38,7 @@ module ScoutAgent
38
38
  # status(status, name = IDCard.me && IDCard.me.process_name)
39
39
  #
40
40
  # Sets the +status+ of this process. You can pass +name+ explicitly if it
41
- # cannot be properly determined from <tt>IDCard.me()</tt>.
41
+ # cannot be properly determined from <tt>IDCard::me()#process_name()</tt>.
42
42
  #
43
43
  def status(*args)
44
44
  status_database.update_status(*args) if status_database
@@ -51,7 +51,7 @@ module ScoutAgent
51
51
  # Clears any status currently set for this process. This should be called
52
52
  # for any process setting status messages <tt>at_exit()</tt>. You can pass
53
53
  # +name+ explicitly if it cannot be properly determined from
54
- # <tt>IDCard.me()</tt>.
54
+ # <tt>IDCard::me()#process_name()</tt>.
55
55
  #
56
56
  def clear_status(*args)
57
57
  status_database.clear_status(*args) if status_database
data/test/tc_plan.rb CHANGED
@@ -99,7 +99,7 @@ class TestPlan < Test::Unit::TestCase
99
99
  end
100
100
 
101
101
  def test_server_url_is_set
102
- assert_match(%r{\Ahttps://}, plan.server_url)
102
+ assert_match(%r{\Ahttps?://}, plan.server_url)
103
103
  end
104
104
 
105
105
  def test_run_as_daemon_is_set
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3
4
+ version: 3.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Edward Gray II
@@ -12,7 +12,7 @@ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
14
 
15
- date: 2009-04-06 00:00:00 -05:00
15
+ date: 2009-04-13 00:00:00 -05:00
16
16
  default_executable:
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
@@ -23,7 +23,7 @@ dependencies:
23
23
  requirements:
24
24
  - - "="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.7.2
26
+ version: 4.7.3
27
27
  version:
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: amalgalite