scout_agent 3.0.0

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.
Files changed (48) hide show
  1. data/AUTHORS +4 -0
  2. data/CHANGELOG +3 -0
  3. data/COPYING +340 -0
  4. data/INSTALL +17 -0
  5. data/LICENSE +6 -0
  6. data/README +3 -0
  7. data/Rakefile +123 -0
  8. data/TODO +3 -0
  9. data/bin/scout_agent +11 -0
  10. data/lib/scout_agent.rb +73 -0
  11. data/lib/scout_agent/agent.rb +42 -0
  12. data/lib/scout_agent/agent/communication_agent.rb +85 -0
  13. data/lib/scout_agent/agent/master_agent.rb +301 -0
  14. data/lib/scout_agent/api.rb +241 -0
  15. data/lib/scout_agent/assignment.rb +105 -0
  16. data/lib/scout_agent/assignment/configuration.rb +30 -0
  17. data/lib/scout_agent/assignment/identify.rb +110 -0
  18. data/lib/scout_agent/assignment/queue.rb +95 -0
  19. data/lib/scout_agent/assignment/reset.rb +91 -0
  20. data/lib/scout_agent/assignment/snapshot.rb +92 -0
  21. data/lib/scout_agent/assignment/start.rb +149 -0
  22. data/lib/scout_agent/assignment/status.rb +44 -0
  23. data/lib/scout_agent/assignment/stop.rb +60 -0
  24. data/lib/scout_agent/assignment/upload_log.rb +61 -0
  25. data/lib/scout_agent/core_extensions.rb +260 -0
  26. data/lib/scout_agent/database.rb +386 -0
  27. data/lib/scout_agent/database/mission_log.rb +282 -0
  28. data/lib/scout_agent/database/queue.rb +126 -0
  29. data/lib/scout_agent/database/snapshots.rb +187 -0
  30. data/lib/scout_agent/database/statuses.rb +65 -0
  31. data/lib/scout_agent/dispatcher.rb +157 -0
  32. data/lib/scout_agent/id_card.rb +143 -0
  33. data/lib/scout_agent/lifeline.rb +243 -0
  34. data/lib/scout_agent/mission.rb +212 -0
  35. data/lib/scout_agent/order.rb +58 -0
  36. data/lib/scout_agent/order/check_in_order.rb +32 -0
  37. data/lib/scout_agent/order/snapshot_order.rb +33 -0
  38. data/lib/scout_agent/plan.rb +306 -0
  39. data/lib/scout_agent/server.rb +123 -0
  40. data/lib/scout_agent/tracked.rb +59 -0
  41. data/lib/scout_agent/wire_tap.rb +513 -0
  42. data/setup.rb +1360 -0
  43. data/test/tc_core_extensions.rb +89 -0
  44. data/test/tc_id_card.rb +115 -0
  45. data/test/tc_plan.rb +285 -0
  46. data/test/test_helper.rb +22 -0
  47. data/test/ts_all.rb +7 -0
  48. metadata +171 -0
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Mission
5
+ class << self
6
+ attr_reader :prepared
7
+ end
8
+
9
+ def self.inherited(new_plugin)
10
+ @prepared = new_plugin
11
+ end
12
+
13
+ #
14
+ # You can use this method to indicate one or more libraries your plugin
15
+ # requires:
16
+ #
17
+ # class MyNeedyPlugin < Scout::Plugin
18
+ # needs "faster_csv", "elif"
19
+ # # ...
20
+ # end
21
+ #
22
+ # Your build_report() method will not be called if all libraries cannot
23
+ # be loaded. RubyGems will be loaded if needed to find the libraries.
24
+ #
25
+ def self.needs(*libraries)
26
+ if libraries.empty?
27
+ @needs ||= [ ]
28
+ else
29
+ needs.push(*libraries.flatten)
30
+ end
31
+ end
32
+
33
+ # Creates a new Scout Plugin to run.
34
+ def initialize(id, name, last_run, memory, options)
35
+ @id = id
36
+ @name = name
37
+ @last_run = last_run
38
+ @memory = memory
39
+ @options = options
40
+ @mission_log_db = Database.load(:mission_log)
41
+ @queue_db = Database.load(:queue)
42
+ end
43
+
44
+ attr_reader :last_run
45
+
46
+ def option(name)
47
+ @options[name] ||
48
+ @options[name.is_a?(String) ? name.to_sym : String(name)]
49
+ end
50
+
51
+ # Builds the data to send to the server.
52
+ #
53
+ # We programatically define several helper methods for creating this data.
54
+ #
55
+ # Usage:
56
+ #
57
+ # reports << {:data => "here"}
58
+ # report(:data => "here")
59
+ # add_report(:data => "here")
60
+ #
61
+ # alerts << {:subject => "subject", :body => "body"}
62
+ # alert("subject", "body")
63
+ # alert(:subject => "subject", :body => "body")
64
+ # add_alert("subject", "body")
65
+ # add_alert(:subject => "subject", :body => "body")
66
+ #
67
+ # errors << {:subject => "subject", :body => "body"}
68
+ # error("subject", "body")
69
+ # error(:subject => "subject", :body => "body")
70
+ # add_error("subject", "body")
71
+ # add_error(:subject => "subject", :body => "body")
72
+ #
73
+ def data_for_server
74
+ @data_for_server ||= { :reports => [ ],
75
+ :hints => [ ],
76
+ :alerts => [ ],
77
+ :errors => [ ],
78
+ :memory => { } }
79
+ end
80
+
81
+ %w[report hint alert error].each do |kind|
82
+ class_eval <<-END
83
+ def #{kind}s
84
+ data_for_server[:#{kind}s]
85
+ end
86
+
87
+ if %w[report hint].include? "#{kind}"
88
+ def #{kind}(new_entry)
89
+ #{kind}s << new_entry
90
+ end
91
+ else
92
+ def #{kind}(*fields)
93
+ #{kind}s << ( fields.first.is_a?(Hash) ?
94
+ fields.first :
95
+ {:subject => fields.first, :body => fields.last} )
96
+ end
97
+ end
98
+ alias_method :add_#{kind}, :#{kind}
99
+ END
100
+ end
101
+
102
+ #
103
+ # Usage:
104
+ #
105
+ # memory(:no_track)
106
+ # memory.delete(:no_track)
107
+ # memory.clear
108
+ #
109
+ def memory(name = nil)
110
+ if name.nil?
111
+ data_for_server[:memory]
112
+ else
113
+ @memory[name] ||
114
+ @memory[name.is_a?(String) ? name.to_sym : String(name)]
115
+ end
116
+ end
117
+
118
+ #
119
+ # Usage:
120
+ #
121
+ # remember(:name, value)
122
+ # remember(:name1, value1, :name2, value2)
123
+ # remember(:name => value)
124
+ # remember(:name1 => value1, :name2 => value2)
125
+ # remember(:name1, value1, :name2 => value2)
126
+ #
127
+ def remember(*args)
128
+ hashes, other = args.partition { |value| value.is_a? Hash }
129
+ hashes.each { |hash| memory.merge!(hash) }
130
+ (0...other.size).step(2) { |i| memory.merge!(other[i] => other[i + 1]) }
131
+ end
132
+
133
+ def each_queued_message(&block)
134
+ while message = @queue_db.peek(@id)
135
+ if block.arity == 1
136
+ block[message[:fields]]
137
+ else
138
+ block[message[:fields], message[:created_at]]
139
+ end
140
+ @queue_db.dequeue(message[:id]) or return # prevent infinite loop
141
+ end
142
+ end
143
+
144
+ #
145
+ # Old plugins will work because they override this method. New plugins can
146
+ # now leave this method in place, add a build_report() method instead, and
147
+ # use the new helper methods to build up content inside which will
148
+ # automatically be returned as the end result of the run.
149
+ #
150
+ def run
151
+ #
152
+ # run the plugin in a new Thread so signals will be handled in the main
153
+ # Thread and not accidentally caught by rescue clauses in the body of the
154
+ # mission
155
+ #
156
+ Thread.new do
157
+ Thread.current.abort_on_exception = true
158
+ if self.class.needs.all? { |l| library_available?(l) }
159
+ begin
160
+ build_report
161
+ rescue Exception
162
+ raise if $!.is_a? SystemExit # don't catch exit() calls
163
+ error "#{@name} run failed with an error", <<-END_ERROR.trim
164
+ Error:
165
+ #{$!.class}
166
+ Message:
167
+ #{$!.message}
168
+ Backtrace:
169
+ #{$!.backtrace.map { |line| " #{line}\n" }.join}
170
+ END_ERROR
171
+ end
172
+ end
173
+ end.join # wait for completion so we have reports to write
174
+ write_reports_and_update_memory
175
+ end
176
+
177
+ private
178
+
179
+ #
180
+ # Returns true is a library can be loaded. A bare require is made as the
181
+ # first attempt. If that fails, RubyGems is loaded and another attempt is
182
+ # made. If the library cannot be loaded either way, an error() is generated
183
+ # and build_report() will not be called.
184
+ #
185
+ def library_available?(library)
186
+ begin
187
+ require library
188
+ rescue LoadError
189
+ begin
190
+ require "rubygems"
191
+ require library
192
+ rescue LoadError
193
+ error("Failed to load library", "Could not load #{library}")
194
+ return false
195
+ end
196
+ end
197
+ true
198
+ end
199
+
200
+ def write_reports_and_update_memory
201
+ %w[report hint alert error].each do |type|
202
+ Array(send("#{type}s")).each do |fields|
203
+ @mission_log_db.write_report(@id, type, fields)
204
+ end
205
+ end
206
+ @mission_log_db.update_mission_memory(@id, data_for_server[:memory])
207
+ end
208
+ end
209
+
210
+ # An external alias.
211
+ Plugin = Mission
212
+ end
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Order
5
+ ORDERS_DIR = LIB_DIR + "order"
6
+
7
+ def self.log=(log)
8
+ @log = log
9
+ end
10
+
11
+ def self.log
12
+ @log ||= WireTap.new(nil)
13
+ end
14
+
15
+ def self.subclasses
16
+ @subclasses ||= Array.new
17
+ end
18
+
19
+ def self.inherited(order)
20
+ subclasses << order
21
+ end
22
+
23
+ def self.load_all
24
+ ORDERS_DIR.each_entry do |order|
25
+ require ORDERS_DIR + order unless order.basename.to_s[0] == ?.
26
+ end
27
+ subclasses
28
+ end
29
+
30
+ def self.can_handle?(message)
31
+ subclasses.each do |order|
32
+ if match_details = order.match?(message)
33
+ return order.new(message, match_details)
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.match?(message)
39
+ const_defined?(:MATCH_RE) and const_get(:MATCH_RE).match(message.body)
40
+ end
41
+
42
+ def initialize(message, match_details)
43
+ @message = message
44
+ @match_details = match_details
45
+ end
46
+
47
+ attr_reader :message, :match_details
48
+
49
+ def log
50
+ Order.log
51
+ end
52
+
53
+ def execute
54
+ raise NotImplementedError,
55
+ "Subclasses must override ScoutAgent::Order#execute()."
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Order
5
+ class CheckInOrder < Order
6
+ MATCH_RE = /\A\s*check[-_]?in(?:\s+(.+?))?\s*\z/
7
+
8
+ def self.mission_log
9
+ return @mission_log if defined? @mission_log
10
+ unless db = Database.load(:mission_log, log)
11
+ log.fatal("Could not load mission log database.")
12
+ exit
13
+ end
14
+ @mission_log = db
15
+ end
16
+
17
+ def self.master_agent
18
+ @master_agent ||= IDCard.new(:master)
19
+ end
20
+
21
+ def execute
22
+ if id_str = match_details.captures.first
23
+ ids = id_str.scan(/\d+/).map { |n| n.to_i }
24
+ unless ids.empty?
25
+ self.class.mission_log.reset_missions(ids)
26
+ end
27
+ end
28
+ self.class.master_agent.signal("ALRM")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Order
5
+ class SnapshotOrder < Order
6
+ MATCH_RE = /\A\s*snap[-_]?shot(?:\s+(.+?))?\s*\z/
7
+
8
+ def self.snapshots
9
+ return @snapshots if defined? @snapshots
10
+ unless db = Database.load(:snapshots, log)
11
+ log.fatal("Could not load snapshots database.")
12
+ exit
13
+ end
14
+ @snapshots = db
15
+ end
16
+
17
+ def self.master_agent
18
+ @master_agent ||= IDCard.new(:master)
19
+ end
20
+
21
+ def execute
22
+ # if id_str = match_details.captures.first
23
+ # ids = id_str.scan(/\d+/).map { |n| n.to_i }
24
+ # unless ids.empty?
25
+ # self.class.mission_log.reset_missions(ids)
26
+ # end
27
+ # end
28
+ API.take_snapshot
29
+ self.class.master_agent.signal("ALRM")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ #
5
+ # This constant holds all global configuration for the agent. There's only
6
+ # this one instance of this data and all systems share it. This data is
7
+ # configured at startup and should never again change without a restart.
8
+ #
9
+ # Users are free to add to this configuration as need (via their configuration
10
+ # file) and make use of those new values in their plugins.
11
+ #
12
+ class << Plan = OpenStruct.new
13
+ ################
14
+ ### Defaults ###
15
+ ################
16
+
17
+ # The default configuration for this agent.
18
+ def defaults
19
+ [ [:server_url, "https://scoutapp.com"],
20
+ [:run_as_daemon, true],
21
+ [:logging_level, "INFO"],
22
+ [:test_mode, false],
23
+
24
+ [:user_choices, %w[daemon nobody]],
25
+ [:group_choices, %w[daemon nogroup]],
26
+ [:prefix_path, "/"],
27
+ [:os_config_path, "etc"],
28
+ [:os_db_path, "var/db"],
29
+ [:os_pid_path, "var/run"],
30
+ [:os_log_path, "var/log"],
31
+ [:config_file, nil],
32
+ [:db_dir, nil],
33
+ [:pid_dir, nil],
34
+ [:log_dir, nil] ]
35
+ end
36
+
37
+ # This method is used to set or reset the default configuration.
38
+ def set_defaults
39
+ defaults.each { |name, value| send("#{name}=", value) }
40
+ end
41
+ alias_method :reset_defaults, :set_defaults
42
+
43
+ # Provide a more natural interface for a boolean flag.
44
+ def run_as_daemon? # can't use alias_method() because it's not defined yet
45
+ run_as_daemon
46
+ end
47
+
48
+ # Provide a more natural interface for a boolean flag.
49
+ def test_mode? # can't use alias_method() because it's not defined yet
50
+ test_mode
51
+ end
52
+
53
+ ###########################
54
+ ### Configuration Files ###
55
+ ###########################
56
+
57
+ # This method loads configuration settings from a plain Ruby file.
58
+ def update_from_config_file(path = config_file)
59
+ eval <<-END_UPDATE
60
+ config = self
61
+ #{File.read(path)}
62
+ config
63
+ END_UPDATE
64
+ end
65
+
66
+ #
67
+ # This method places the current configuration into a standard configuration
68
+ # file. This makes it easy for users to edit this file and modify all
69
+ # future runs of the agent.
70
+ #
71
+ def write_default_config_file
72
+ config_file.dirname.mkpath
73
+ config_file.open(File::CREAT|File::EXCL|File::WRONLY) do |pid|
74
+ pid.puts <<-END_DEFAULT_CONFIG.trim
75
+ #!/usr/bin/env ruby -wKU
76
+
77
+ #
78
+ # This file configures #{ScoutAgent.proper_agent_name}. The settings in
79
+ # here should get you started. You can tweak these as the need arises.
80
+ #
81
+ # Any Ruby code you would like run at start-up is a valid addition
82
+ # to this file.
83
+ #
84
+
85
+ ####################
86
+ ### Basic Config ###
87
+ ####################
88
+
89
+ # The key below is how the server identifies this machine:
90
+ config.agent_key = #{agent_key.inspect}
91
+
92
+ # The following is the URL used to reach the server:
93
+ config.server_url = #{server_url.inspect}
94
+
95
+ #
96
+ # When the following is true, the agent will disconnect from the
97
+ # terminal that launches it and run in the background. You can
98
+ # switch this to false to have the program stay connected and log
99
+ # to STDOUT. This can be handy when debugging issues.
100
+ #
101
+ # You can set this option to false for a single run with the
102
+ # command-line switch --no-daemon.
103
+ #
104
+ config.run_as_daemon = #{run_as_daemon.inspect}
105
+
106
+ #
107
+ # The following sets your logging level for the agent. This setting can
108
+ # be one of "DEBUG", "INFO", "WARN", "ERROR", or "FATAL". Messages
109
+ # below the seleted level will not be written to the logs. "DEBUG" is
110
+ # intended for the developers as it includes some very low level data,
111
+ # "INFO" (the default) is good general purpose logging that will what
112
+ # the agent does as it works, and "WARN" is a good choice if you want
113
+ # to minimize log space taken but still see problems. Logs are rotated
114
+ # daily and deleted after seven days though, so you shouldn't need to
115
+ # worry about the agent filling up your hard drive.
116
+ #
117
+ # This agent includes a command to upload logs to our server. This is
118
+ # never done automatically or without your permission, but it can help
119
+ # us troubleshoot issues on your box. We will give you the proper
120
+ # command for this during a support request if we think it would help.
121
+ # Rest assured that agent logs do not contain sensative data.
122
+ #
123
+ config.logging_level = #{logging_level.inspect}
124
+
125
+ #####################
126
+ ### Expert Config ###
127
+ #####################
128
+
129
+ #
130
+ # The agent will try to use standard Unix locations to store its
131
+ # data, but you are free to adjust these paths as needed.
132
+ #
133
+ # The prefix is prepended to all other paths, so you can change
134
+ # just that to move all agent related storage. The other three
135
+ # paths are specific locations where resources will be stored,
136
+ # so you can adjust those separately if needed.
137
+ #
138
+ # Note: to have the prefix effect the location of this file or
139
+ # to set your OS's configuration path, you will need to use
140
+ # command-line switches (--prefix and --os-config-path respectively).
141
+ # It wouldn't help to have those settings in this file, for obvious
142
+ # reasons.
143
+ #
144
+ config.prefix_path = #{prefix_path.to_s.inspect}
145
+
146
+ config.os_db_path = #{os_db_path.
147
+ relative_path_from(prefix_path).to_s.inspect}
148
+ config.os_pid_path = #{os_pid_path.
149
+ relative_path_from(prefix_path).to_s.inspect}
150
+ config.os_log_path = #{os_log_path.
151
+ relative_path_from(prefix_path).to_s.inspect}
152
+
153
+ #
154
+ # The agent must be started with super user privileges so it can
155
+ # prepare the environment for a long run. However, it will abandon
156
+ # these privileges once setup is complete to better protect your
157
+ # security.
158
+ #
159
+ # The following are the list of users and groups the agent will
160
+ # try to switch to. Choices are tried from left to right, so by
161
+ # default the "daemon" user is used but the agent will try
162
+ # "nobody" if daemon is not available. These are standard users
163
+ # and groups for processes like this that run on Unix.
164
+ #
165
+ # If you wish to improve security even more, we recommend creating
166
+ # a user and group called "#{ScoutAgent.agent_name}" and replacing these
167
+ # defaults with what you created. This puts you full control
168
+ # of what the agent will have access to on your system and makes
169
+ # it easier to track what the agent is doing.
170
+ #
171
+ config.user_choices = %w[#{user_choices.join(" ")}]
172
+ config.group_choices = %w[#{group_choices.join(" ")}]
173
+
174
+ ############################
175
+ ### Your Personal Config ###
176
+ ############################
177
+
178
+ # Add any additional Ruby code you need to run at start-up below...
179
+ END_DEFAULT_CONFIG
180
+ end
181
+ config_file.chmod(0755)
182
+ true
183
+ rescue Errno::EEXIST # file already exists
184
+ false
185
+ rescue Errno::EACCES # don't have permission
186
+ false
187
+ end
188
+
189
+ #############################
190
+ ### Command-line Switches ###
191
+ #############################
192
+
193
+ # This method allows mass update through a +hash+ of settings.
194
+ def update_from_switches(hash)
195
+ hash.each { |name, value| send("#{name}=", value) }
196
+ end
197
+ alias_method :update_from_hash, :update_from_switches
198
+
199
+ #############
200
+ ### Paths ###
201
+ #############
202
+
203
+ # A prefix used in all configuration paths.
204
+ def prefix_path
205
+ Pathname.new(super)
206
+ end
207
+
208
+ #
209
+ # This code defines OS specific path accessors that honor the +prefix_path+.
210
+ # These paths prefix all of the specific application paths.
211
+ #
212
+ %w[os_config_path os_db_path os_pid_path os_log_path].each do |path|
213
+ define_method(path) do
214
+ prefix_path + super
215
+ end
216
+ end
217
+
218
+ #
219
+ # The full path to the loaded configuration file, prefixed by +prefix_path+
220
+ # and the +os_config_path+. By default, this file is named after the agent.
221
+ #
222
+ def config_file
223
+ os_config_path + (super || "#{ScoutAgent.agent_name}.rb")
224
+ end
225
+
226
+ #
227
+ # This code defines the application specific directory paths, prefixed by
228
+ # +prefix_path+ and the related OS directory. By default, these directories
229
+ # are named after the agent.
230
+ #
231
+ %w[db pid log].each do |path|
232
+ define_method("#{path}_dir") do
233
+ send("os_#{path}_path") + (super || ScoutAgent.agent_name)
234
+ end
235
+ end
236
+
237
+ #
238
+ # This code creates builders for the configuration directories. These
239
+ # methods build the directories and set their group and permissons so
240
+ # they can be written to after the daemon surrenders super user privileges.
241
+ #
242
+ { :db_dir => 0777,
243
+ :pid_dir => 0775,
244
+ :log_dir => 0777 }.each do |path, permissions|
245
+ define_method("build_#{path}") do |group_id|
246
+ begin
247
+ send(path).mkpath
248
+ send(path).chown(nil, group_id)
249
+ send(path).chmod(permissions)
250
+ true
251
+ rescue Errno::EACCES, Errno::EPERM # don't have permission
252
+ false
253
+ end
254
+ end
255
+ end
256
+
257
+ #############
258
+ ### URL's ###
259
+ #############
260
+
261
+ def agent_url
262
+ URI.join(server_url, "clients/#{agent_key}")
263
+ end
264
+
265
+ #################
266
+ ### Databases ###
267
+ #################
268
+
269
+ def prepare_global_database(name)
270
+ db = Database.load(name) or return false
271
+ db.path.chmod(0777)
272
+ true
273
+ rescue Errno::EPERM # don't have permission
274
+ false
275
+ end
276
+
277
+ ##################
278
+ ### Validation ###
279
+ ##################
280
+
281
+ # Returns +true+ if all needed configuration paths exist.
282
+ def present?
283
+ %w[config_file db_dir log_dir].all? { |path| send(path).exist? } and
284
+ %w[queue snapshots].all? { |db| Database.path(db).exist? }
285
+ end
286
+
287
+ #
288
+ # Returns +true+ if <tt>present?</tt> and all needed configuration paths
289
+ # have proper permisions as far as this current run is concerned.
290
+ #
291
+ def valid?
292
+ present? and
293
+ %w[config_file db_dir log_dir].all? { |path| send(path).readable? } and
294
+ %w[db_dir log_dir].all? { |path| send(path).writable? } and
295
+ %w[queue snapshots].map { |db| Database.path(db) }.
296
+ all? { |path| path.readable? and
297
+ path.writable? }
298
+ end
299
+ end
300
+
301
+ #
302
+ # Set the defaults after we have overriden some accessors, to keep OpenStruct
303
+ # from replacing them.
304
+ #
305
+ Plan.set_defaults
306
+ end