scout_agent 3.0.0

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