scout_agent 3.0.6 → 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
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}'.