scout_agent 3.0.6 → 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ == 3.0.7
2
+
3
+ * Added a log() method (with a logger() alias) plugins can access to add
4
+ messages to the log as needed
5
+ * Added JSON hooks for the Time class so they will be converted back to Ruby
6
+ objects when loaded
7
+ * Added a `scout_agent test` command for plugin developers
8
+ * Switched to the default URL of https://beta.scoutapp.com
9
+ * Improved the error handling in the communications agent for connection failure
10
+ scenarios like blocked ports (fixes a CPU pegging bug)
11
+ * Loosened error checking when killing processes to improve monitor stability
12
+ * Loosened the check-in timeout a touch as the monitor seemed to kill off
13
+ processes a little too quickly
14
+ * Enhanced the monitor to kill off mission processes after killing the mission
15
+ runner
16
+ * Made the stop command hunt for stray processes after stopping the monitor
17
+ process
18
+
1
19
  == 3.0.6
2
20
 
3
21
  * The new `sudo scout_agent update` command can be used to update your
data/Rakefile CHANGED
@@ -81,7 +81,7 @@ end
81
81
 
82
82
  Rake::RDocTask.new do |rdoc|
83
83
  rdoc.main = "README"
84
- rdoc.rdoc_dir = "doc/html"
84
+ rdoc.rdoc_dir = "doc"
85
85
  rdoc.title = "Scout Agent Documentation"
86
86
  rdoc.rdoc_files.include( *%w[ README INSTALL TODO CHANGELOG
87
87
  AUTHORS COPYING LICENSE lib/ ] )
@@ -18,6 +18,7 @@ module ScoutAgent
18
18
  @agent_jid = nil
19
19
  @jabber = nil
20
20
  @roster = nil
21
+ @connecting = false
21
22
  @shutdown_thread = nil
22
23
  @trusted = Array(Plan.xmpp_trusted).map { |trusted|
23
24
  trusted_with_server = trusted.sub(/@\*\z/, "@#{jabber_server}")
@@ -36,6 +37,7 @@ module ScoutAgent
36
37
  end
37
38
 
38
39
  def finish
40
+ log.info("Shutting down.")
39
41
  if @shutdown_thread
40
42
  @shutdown_thread.run
41
43
  else
@@ -51,17 +53,19 @@ module ScoutAgent
51
53
  log.info("Connecting to XMPP: #{@agent_jid}")
52
54
  @jabber = Jabber::Client.new(@agent_jid)
53
55
  @jabber.on_exception do |error, stream, during|
54
- try_connection
56
+ try_connection unless @connecting
55
57
  end
56
58
  try_connection
57
59
  end
58
60
 
59
61
  def try_connection
62
+ @connecting = true
60
63
  until connect_and_authenticate?
61
64
  log.info( "Waiting #{RECONNECT_WAIT} seconds before making another " +
62
65
  "connection attempt." )
63
66
  sleep RECONNECT_WAIT
64
67
  end
68
+ @connecting = false
65
69
  end
66
70
 
67
71
  def connect_and_authenticate?
@@ -271,7 +271,7 @@ module ScoutAgent
271
271
  rescue Exception => error # any compile error
272
272
  raise if $!.is_a? SystemExit # don't catch exit() calls
273
273
  log.error( "#{mission[:name]} could not be compiled: " +
274
- "#{error.message} (#{error.class})." )
274
+ "#{error.message} (#{error.class})." )
275
275
  reported = @db.write_report(
276
276
  mission[:id],
277
277
  :error,
@@ -287,11 +287,11 @@ module ScoutAgent
287
287
  if prepared = Mission.prepared
288
288
  log.info("Starting #{mission[:name]} mission.")
289
289
  status("Running")
290
- prepared.new( *mission.values_at( :id,
291
- :name,
292
- :last_run_at,
293
- :memory,
294
- :options ) ).run
290
+ prepared.new( *( mission.values_at( :id,
291
+ :name,
292
+ :last_run_at,
293
+ :memory,
294
+ :options ) + [@log] ) ).run
295
295
  else # no mission loaded
296
296
  log.error("#{mission[:name]} could not be prepared.")
297
297
  reported = @db.write_report(
@@ -2,9 +2,17 @@
2
2
  # encoding: UTF-8
3
3
 
4
4
  module ScoutAgent
5
+ #
6
+ # An Agent is a subprocess launched at startup and kept running by a Lifeline
7
+ # monitor. These subprocesses manage the major functions of the platform.
8
+ #
5
9
  class Agent
6
10
  include Tracked
7
11
 
12
+ #
13
+ # Prepares a new agent instance by building a log() and setting a
14
+ # <tt>"Loading"</tt> status message.
15
+ #
8
16
  def initialize
9
17
  @log = ScoutAgent.prepare_wire_tap(file_name)
10
18
  log.info("Loading.")
@@ -15,27 +23,40 @@ module ScoutAgent
15
23
  end
16
24
  end
17
25
 
26
+ # The log file this agent will report to.
18
27
  attr_reader :log
19
28
 
29
+ # Ensure we are the only process with this name running.
20
30
  def authorize
21
31
  IDCard.new(file_name).authorize
22
32
  end
23
33
 
24
- def run
25
- raise NotImplementedError,
26
- "Subclasses must override ScoutAgent::Agent#run()."
27
- end
28
-
34
+ #
35
+ # This method will be called when the agent process receives an +ALRM+
36
+ # signal. By default, this method is a no-op, but subclasses can overrided
37
+ # it to add behavior.
38
+ #
29
39
  def notice_changes
30
40
  # do nothing: specific agents can override for their purposes
31
41
  end
32
42
 
43
+ #
44
+ # This method will be called when the agent receives a +TERM+ signal. The
45
+ # default behavior is to exit(), but subclasses can override it to add more
46
+ # specific behavior.
47
+ #
33
48
  def finish
34
49
  exit
35
50
  end
36
51
 
52
+ #######
37
53
  private
54
+ #######
38
55
 
56
+ #
57
+ # Returns the file name to be used for this agent's PID files and status
58
+ # entries.
59
+ #
39
60
  def file_name
40
61
  self.class.short_name.sub(/Agent\z/, "").snake_case
41
62
  end
@@ -18,8 +18,8 @@ end
18
18
  module ScoutAgent
19
19
  #
20
20
  # This module contains methods used to communicate with the agent
21
- # programmatically. These methods can be used to send data to the agent or
22
- # to make requests that the agent perform certain actions for you.
21
+ # programmatically. These methods can be used to send data to the agent or to
22
+ # make requests that the agent perform certain actions for you.
23
23
  #
24
24
  module API
25
25
  #
@@ -29,6 +29,11 @@ module ScoutAgent
29
29
  # or failure of your requests.
30
30
  #
31
31
  class Command
32
+ #
33
+ # Prepares and runs an API command by shelling out to +name+, optionally
34
+ # with +args+ or +input+. You can also set +options+ for the run, like
35
+ # <tt>:background => true</tt>.
36
+ #
32
37
  def initialize(name, args, input, options) # :nodoc:
33
38
  @name = name
34
39
  @args = args
@@ -68,20 +73,26 @@ module ScoutAgent
68
73
  @finished
69
74
  end
70
75
 
76
+ #######
71
77
  private
78
+ #######
72
79
 
80
+ #
81
+ # Runs the prepared command, setting +exit_status+, +error_message+, and
82
+ # +finished+ on completion with the results of the run.
83
+ #
73
84
  def run
74
85
  command = [ API.path_to_ruby, API.path_to_agent,
75
86
  @name, *Array(@args) ].
76
87
  map { |s| "'#{API.shell_escape(s)}'" }.join(" ")
77
88
  begin
78
- response = open("| #{command} 2>&1", "r+") do |agent|
89
+ response = open("| #{command} 2>&1", "r+") { |agent|
79
90
  agent.puts @input.to_json if @input
80
91
  agent.close_write
81
92
  agent.read
82
- end
93
+ }
83
94
  rescue Exception => error # we cannot shell out to the agent
84
- @exit_status = ($? && $?.exitstatus) || -1
95
+ @exit_status = ($? && $?.exitstatus) || 1
85
96
  @error_message = "#{error.message} (#{error.class})"
86
97
  @finished = true
87
98
  return
@@ -91,19 +102,37 @@ module ScoutAgent
91
102
  @finished = true
92
103
  end
93
104
 
105
+ # Wraps run() in a background Thread.
94
106
  def run_in_background
95
- Thread.new { run }
107
+ Thread.new do
108
+ run
109
+ end
96
110
  end
97
111
  end
98
112
 
113
+ #
114
+ # This is a private specialization of Command that adds some validation for
115
+ # queue commands in order to fail faster during API calls.
116
+ #
99
117
  class QueueCommand < Command # :nodoc:
118
+ #
119
+ # Simplifies the needed parameters to be specific to queue commands.
120
+ # Validation is also invoked here before the command is run.
121
+ #
100
122
  def initialize(mission_id_or_report_type, fields, options)
101
123
  validate(mission_id_or_report_type, fields)
102
124
  super(:queue, [mission_id_or_report_type], fields, options)
103
125
  end
104
126
 
127
+ #######
105
128
  private
129
+ #######
106
130
 
131
+ #
132
+ # Duplicates some error checking done by the queue command, so that
133
+ # bad calls can fail before we pay the penalty of shelling out to the
134
+ # agent.
135
+ #
107
136
  def validate(mission_id_or_report_type, fields)
108
137
  unless mission_id_or_report_type.to_s =~
109
138
  /\A(?:report|hint|alert|error|\d*[1-9])\z/
@@ -125,7 +154,13 @@ module ScoutAgent
125
154
  end
126
155
  end
127
156
 
157
+ ###############
128
158
  module_function
159
+ ###############
160
+
161
+ ###############
162
+ ### Helpers ###
163
+ ###############
129
164
 
130
165
  #
131
166
  # This convience method will escape a single _word_ for use in a shell
@@ -161,6 +196,10 @@ module ScoutAgent
161
196
  *%w[.. .. bin scout_agent] ) )
162
197
  end
163
198
 
199
+ ###############
200
+ ### Queuing ###
201
+ ###############
202
+
164
203
  #
165
204
  # Use this method to queue a +message+ for a mission to receive on it's next
166
205
  # run.
@@ -177,7 +216,7 @@ module ScoutAgent
177
216
  # end
178
217
  #
179
218
  # If you don't wish to wait, you can request that the send take place in the
180
- # background. You can still use the command objects to check the status of
219
+ # background. You can still use the Command object to check the status of
181
220
  # these requests, but you must first wait for them to be finished?(). Do
182
221
  # not trust any other methods, like success?(), until finished?() returns
183
222
  # +true+.
@@ -242,6 +281,10 @@ module ScoutAgent
242
281
  QueueCommand.new(:error, fields, options)
243
282
  end
244
283
 
284
+ #################
285
+ ### Snapshots ###
286
+ #################
287
+
245
288
  #
246
289
  # :call-seq:
247
290
  # take_snapshot(options = { })
@@ -253,16 +296,13 @@ module ScoutAgent
253
296
  # during the next checkin. The returned object and background processing
254
297
  # rules are the same as those described in queue_for_mission().
255
298
  #
256
- # Passing a `true` value in `force` clears the command times before running,
299
+ # Passing a +true+ value in +force+ clears the command times before running,
257
300
  # forcing a full snapshot to be taken.
258
301
  #
259
302
  def take_snapshot(*args)
260
- started = Time.now
261
303
  force = args.shift ? %w[force] : nil unless args.first.is_a? Hash
262
- options = args.shift || { }
304
+ options = args.shift || Hash.new
263
305
  Command.new(:snapshot, force, nil, options)
264
- ensure
265
- p Time.now - started
266
306
  end
267
307
  end
268
308
  end
@@ -3,7 +3,18 @@
3
3
 
4
4
  module ScoutAgent
5
5
  class Assignment
6
+ #
7
+ # Invoke with:
8
+ #
9
+ # scout_agent config
10
+ #
11
+ # This command shows the current configuration of the agent mainly
12
+ # determined by reading the configuration file. However, you can pass
13
+ # command-line switches when running this command to see how they change
14
+ # things and help you find a good setup for other commands.
15
+ #
6
16
  class Configuration < Assignment
17
+ # Runs the configuration command.
7
18
  def execute
8
19
  puts "Configuration"
9
20
  puts "============="
@@ -3,15 +3,27 @@
3
3
 
4
4
  module ScoutAgent
5
5
  class Assignment
6
+ #
7
+ # Invoke with:
8
+ #
9
+ # sudo scout_agent id
10
+ #
11
+ # This command prepares the agent for use by recording your key and settings
12
+ # into a configuration file. This file is then loaded by all other commands
13
+ # to configure the agent for use. Have a look at <tt>scout_agent -h</tt>
14
+ # for other options you may wish to set, like +proxy+.
15
+ #
6
16
  class Identify < Assignment
7
17
  plan :switches_only
8
18
  choose_group true
9
19
 
20
+ # Runs the identify command.
10
21
  def execute
11
22
  puts "Identifying Your Agent"
12
23
  puts "======================"
13
24
  puts
14
25
 
26
+ # make sure we can access the needed directories
15
27
  %w[config_file db_dir log_dir].each do |path|
16
28
  full = dir = Plan.send(path)
17
29
  loop do
@@ -23,6 +35,7 @@ module ScoutAgent
23
35
  end
24
36
  end
25
37
 
38
+ # get a key and test the server connection with that key
26
39
  unless Plan.config_file.exist?
27
40
  print <<-END_KEY_DESCRIPTION.trim.to_question
28
41
  I need your Agent Key displayed in the Agent Settings tab
@@ -42,6 +55,7 @@ module ScoutAgent
42
55
  end
43
56
  end
44
57
 
58
+ # write the configuration file
45
59
  puts "Saving identification..."
46
60
  if Plan.write_default_config_file
47
61
  puts "Identification file '#{Plan.config_file}' created."
@@ -52,6 +66,7 @@ module ScoutAgent
52
66
  abort_with_insufficient_permissions(Plan.config_file)
53
67
  end
54
68
  end
69
+ # create directories and global databases
55
70
  %w[db_dir log_dir].each do |path|
56
71
  dir = Plan.send(path)
57
72
  if dir.exist?
@@ -77,6 +92,7 @@ module ScoutAgent
77
92
  puts "Done."
78
93
  puts
79
94
 
95
+ # show next steps
80
96
  puts <<-END_START_INSTRUCTIONS.trim
81
97
  You are now identified. You can start the agent with:
82
98
 
@@ -85,8 +101,14 @@ module ScoutAgent
85
101
  END_START_INSTRUCTIONS
86
102
  end
87
103
 
104
+ #######
88
105
  private
106
+ #######
89
107
 
108
+ #
109
+ # Abort with an error message to the user that says we lack the
110
+ # permissions to configure their agent.
111
+ #
90
112
  def abort_with_insufficient_permissions(path)
91
113
  abort <<-END_PRIVILEGES.trim
92
114
  I don't have enough privileges to create '#{path}'.
@@ -97,6 +119,10 @@ module ScoutAgent
97
119
  END_PRIVILEGES
98
120
  end
99
121
 
122
+ #
123
+ # Abort with an error message to the user that says tells them their key
124
+ # is probably not valid.
125
+ #
100
126
  def abort_with_bad_key
101
127
  abort <<-END_BAD_KEY.trim
102
128
  Could not contact server. The key may be incorrect.
@@ -6,7 +6,27 @@ require "io/wait"
6
6
 
7
7
  module ScoutAgent
8
8
  class Assignment
9
+ #
10
+ # Invoke with:
11
+ #
12
+ # scout_agent q ID_OR_TYPE <<< '{"fields": "in JSON"}'
13
+ #
14
+ # This command queues some data from an external source for a plugin. The
15
+ # plugin to receive the message is given by ID in +ID_OR_TYPE+. That plugin
16
+ # will be able to collect this data during its next run.
17
+ #
18
+ # Alternately, this command can be used to send a complete report, alert,
19
+ # error, or hint directly to the server (with the next check-in). When
20
+ # using as such, +ID_OR_TYPE+ must be one of those types and the fields must
21
+ # be a Hash (object in JSON terminology) that includes the key "plugin_id"
22
+ # associated with an ID value for the reporting plugin.
23
+ #
9
24
  class Queue < Assignment
25
+ #
26
+ # A list of errors that can be sent to the user when attempting to queue a
27
+ # message. The index of the message plus one is also the exit status for
28
+ # that error.
29
+ #
10
30
  ERRORS = [ [ :missing_db,
11
31
  "Queue database could not be loaded." ],
12
32
  [ :missing_id,
@@ -27,19 +47,24 @@ module ScoutAgent
27
47
  [ :failed_to_queue_message,
28
48
  "Your message could not be queued at this time." ] ]
29
49
 
50
+ # Runs the queue command.
30
51
  def execute
52
+ # prepare the log
31
53
  log = ScoutAgent.prepare_wire_tap(:queue, :skip_stdout)
32
54
 
55
+ # record our status and set removal at_exit()
33
56
  status_database(log)
34
57
  status("Queuing message", :queue)
35
58
  at_my_exit do
36
59
  clear_status(:queue)
37
60
  end
38
61
 
62
+ # load the queue database
39
63
  unless db = Database.load(:queue, log)
40
64
  abort_with_error(:missing_db)
41
65
  end
42
66
 
67
+ # ensure id or type is valid
43
68
  log.info("Validating message for queuing.")
44
69
  unless id = Array(other_args).shift
45
70
  abort_with_error(:missing_id)
@@ -48,6 +73,7 @@ module ScoutAgent
48
73
  abort_with_error(:invalid_id)
49
74
  end
50
75
 
76
+ # read field data and parse JSON
51
77
  fields = ARGF.read unless ARGV.empty? and not $stdin.ready?
52
78
  if fields.nil? or fields.empty?
53
79
  abort_with_error(:missing_fields)
@@ -59,6 +85,7 @@ module ScoutAgent
59
85
  abort_with_error(:invalid_fields)
60
86
  end
61
87
 
88
+ # ensure we have a valid plugin_id, if needed
62
89
  if %w[report hint alert error].include? id
63
90
  unless fields.is_a? Hash
64
91
  abort_with_error(:invalid_report_fields)
@@ -74,18 +101,26 @@ module ScoutAgent
74
101
  log.info("Message is valid (#{bytes} bytes).")
75
102
  end
76
103
 
104
+ # queue the message
77
105
  log.info("Queuing message.")
78
106
  unless db.enqueue(id, fields)
79
107
  abort_with_error(:failed_to_queue_message)
80
108
  end
81
109
 
110
+ # maintain the queue database
82
111
  db.maintain
83
112
 
84
113
  log.info("Messages queued successfully.")
85
114
  end
86
115
 
116
+ #######
87
117
  private
118
+ #######
88
119
 
120
+ #
121
+ # Abort with an error message and exit status that tells the user which
122
+ # part of the process failed.
123
+ #
89
124
  def abort_with_error(error_name)
90
125
  error = ERRORS.assoc(error_name)
91
126
  warn error.last
@@ -3,8 +3,20 @@
3
3
 
4
4
  module ScoutAgent
5
5
  class Assignment
6
+ #
7
+ # Invoke with:
8
+ #
9
+ # sudo scout_agent reset
10
+ #
11
+ # This command removes all saved configuration and data from the agent.
12
+ # After this is run it's basically like the agent has been reset to the way
13
+ # it was after being intalled. This is a dangerous command that destroys
14
+ # data and cannot be undone.
15
+ #
6
16
  class Reset < Assignment
17
+ # Runs the reset command.
7
18
  def execute
19
+ # make sure the agent isn't running
8
20
  agent = IDCard.new(:lifeline)
9
21
  if agent.pid_file.exist?
10
22
  abort_with_agent_running
@@ -14,6 +26,7 @@ module ScoutAgent
14
26
  puts "================"
15
27
  puts
16
28
 
29
+ # get confirmation from the user
17
30
  print <<-END_WARNING.trim.to_question
18
31
  This is a dangerous operation that will remove configuration
19
32
  and data files. Are you absolutely sure you wish to remove
@@ -25,6 +38,7 @@ module ScoutAgent
25
38
 
26
39
  puts
27
40
  puts "Resetting..."
41
+ # remove configuration file
28
42
  if not Plan.config_file.exist?
29
43
  puts <<-END_CONFIG_FILE
30
44
  Identification file '#{Plan.config_file}' doesn't exist. Skipped.
@@ -37,6 +51,7 @@ module ScoutAgent
37
51
  abort_with_insufficient_permissions(Plan.config_file)
38
52
  end
39
53
  end
54
+ # remove directories and the data they contain
40
55
  %w[db_dir pid_dir log_dir].each do |path|
41
56
  dir = Plan.send(path)
42
57
  if not dir.exist?
@@ -53,6 +68,7 @@ module ScoutAgent
53
68
  puts "Done."
54
69
  puts
55
70
 
71
+ # show next steps
56
72
  puts <<-END_START_INSTRUCTIONS.trim
57
73
  This agent is reset. You can prepare it for use again
58
74
  at anytime with:
@@ -62,8 +78,14 @@ module ScoutAgent
62
78
  END_START_INSTRUCTIONS
63
79
  end
64
80
 
81
+ #######
65
82
  private
83
+ #######
66
84
 
85
+ #
86
+ # Abort with an error message to the user that says we with a warning that
87
+ # the agent cannot be reset when it is running.
88
+ #
67
89
  def abort_with_agent_running
68
90
  abort <<-END_AGENT_RUNNING.trim
69
91
  You cannot reset while the agent is running. Please stop
@@ -74,10 +96,18 @@ module ScoutAgent
74
96
  END_AGENT_RUNNING
75
97
  end
76
98
 
99
+ #
100
+ # Abort with an error message to the user that confirms their
101
+ # cancellation of the reset.
102
+ #
77
103
  def abort_with_cancel
78
104
  abort "Reset cancelled. No data was removed."
79
105
  end
80
106
 
107
+ #
108
+ # Abort with an error message to the user that says we don't have
109
+ # permission to remove some configuration.
110
+ #
81
111
  def abort_with_insufficient_permissions(path)
82
112
  abort <<-END_PRIVILEGES.trim
83
113
  I don't have enough privileges to remove '#{path}'.