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,241 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ require "pathname" # all paths are Pathname objects
4
+
5
+ begin
6
+ require "json" # the agent communicates in JSON
7
+ rescue LoadError # library not found
8
+ begin
9
+ require "rubygems"
10
+ require "json"
11
+ rescue LoadError # library not found
12
+ abort "Please install the 'json' gem to use this API."
13
+ end
14
+ end
15
+
16
+ module ScoutAgent
17
+ #
18
+ # This module contains methods used to communicate with the agent
19
+ # programmatically. These methods can be used to send data to the agent or
20
+ # to make requests that the agent perform certain actions for you.
21
+ #
22
+ module API
23
+ #
24
+ # You should not need to consruct instances of this class directly as the
25
+ # API will handle those details for you. However, you can use the methods
26
+ # of these instances, returned by many API methods, to examine the success
27
+ # or failure of your requests.
28
+ #
29
+ class Command
30
+ def initialize(name, args, input, options) # :nodoc:
31
+ @name = name
32
+ @args = args
33
+ @input = input
34
+ @background = options[:background]
35
+ @exit_status = nil
36
+ @error_message = nil
37
+ @finished = false
38
+
39
+ @background ? run_in_background : run
40
+ end
41
+
42
+ # Returns the exit status of the agent command as an Integer.
43
+ attr_reader :exit_status
44
+ # This is the error code, if the request was not a success?().
45
+ alias_method :error_code, :exit_status
46
+ #
47
+ # Returns the error message from the agent as a Sting. This is only set
48
+ # if the request was not a success?().
49
+ #
50
+ attr_reader :error_message
51
+
52
+ # Returns +true+ if your request to the agent completed successfully.
53
+ def success?
54
+ exit_status and exit_status.zero?
55
+ end
56
+
57
+ #
58
+ # Returns +true+ if you chose to run the request in the background and it
59
+ # is now complete. You can periodically poll this method to see if a
60
+ # background request has completed yet.
61
+ #
62
+ # *Warning*: none of the other methods should be trusted until this
63
+ # method returns +true+.
64
+ #
65
+ def finished?
66
+ @finished
67
+ end
68
+
69
+ private
70
+
71
+ def run
72
+ command = [API.path_to_agent, @name, *Array(@args)].
73
+ map { |s| "'#{API.shell_escape(s)}'" }.join(" ")
74
+ begin
75
+ response = open("| #{command} 2>&1", "r+") do |agent|
76
+ agent.puts @input.to_json if @input
77
+ agent.close_write
78
+ agent.read
79
+ end
80
+ rescue Exception => error # we cannot shell out to the agent
81
+ @exit_status = ($? && $?.exitstatus) || -1
82
+ @error_message = "#{error.message} (#{error.class})"
83
+ @finished = true
84
+ return
85
+ end
86
+ @exit_status = $?.exitstatus
87
+ @error_message = response unless success?
88
+ @finished = true
89
+ end
90
+
91
+ def run_in_background
92
+ Thread.new { run }
93
+ end
94
+ end
95
+
96
+ class QueueCommand < Command # :nodoc:
97
+ def initialize(mission_id_or_report_type, fields, options)
98
+ validate(mission_id_or_report_type, fields)
99
+ super(:queue, [mission_id_or_report_type], fields, options)
100
+ end
101
+
102
+ private
103
+
104
+ def validate(mission_id_or_report_type, fields)
105
+ unless mission_id_or_report_type.to_s =~
106
+ /\A(?:report|hint|alert|error|\d*[1-9])\z/
107
+ raise ArgumentError, "Invalid mission ID or report type"
108
+ end
109
+
110
+ if %w[report hint alert error].include? mission_id_or_report_type.to_s
111
+ unless fields.is_a? Hash
112
+ raise ArgumentError, "Reports must receive a fields Hash"
113
+ end
114
+ plugin_id = fields[:plugin_id] || fields["plugin_id"]
115
+ unless plugin_id
116
+ raise ArgumentError, "A :plugin_id is a required field for reports"
117
+ end
118
+ unless plugin_id.to_s =~ /\A\d*[1-9]\z/
119
+ raise ArgumentError, "The :plugin_id must be a positive Integer"
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ module_function
126
+
127
+ #
128
+ # This convience method will escape a single _word_ for use in a shell
129
+ # command. This is handy for anyone wanting to construct their own commands
130
+ # manually. You will not need this method if you stick to the higher level
131
+ # interface methods provided by this API.
132
+ #
133
+ def shell_escape(str)
134
+ String(str).gsub(/(?=[^a-zA-Z0-9_.\/\-\x7F-\xFF\n])/n, '\\').
135
+ gsub(/\n/, "'\n'").
136
+ sub(/^$/, "''")
137
+ end
138
+
139
+ #
140
+ # This method returns the path to the agent executable as a Pathname object.
141
+ # This is convience for those who wish to manually communicate with the
142
+ # agent and not needed if you stick to the higher level interface.
143
+ #
144
+ def path_to_agent
145
+ Pathname.new( File.join( File.dirname(__FILE__),
146
+ *%w[.. .. bin scout_agent] ) )
147
+ end
148
+
149
+ #
150
+ # Use this method to queue a +message+ for a mission to receive on it's next
151
+ # run.
152
+ #
153
+ # By default, this method will not return until the request to the agent is
154
+ # complete. It then returns a Command object, which can be used to examine
155
+ # how the request went. For example:
156
+ #
157
+ # response = ScoutAgent::API.queue_for_mission(42, {"message" => "here"})
158
+ # if response.success?
159
+ # puts "Message queued."
160
+ # else
161
+ # warn "Error: #{response.error_message} (#{response.error_code})"
162
+ # end
163
+ #
164
+ # If you don't wish to wait, you can request that the send take place in the
165
+ # background. You can still use the command objects to check the status of
166
+ # these requests, but you must first wait for them to be finished?(). Do
167
+ # not trust any other methods, like success?(), until finished?() returns
168
+ # +true+.
169
+ #
170
+ # in_progress = ScoutAgent::API.queue_for_mission( ...,
171
+ # :background => true )
172
+ # # the above returns immediately, so we need to wait for it to finish
173
+ # until in_progress.finished?
174
+ # # do important work that can't wait here
175
+ # end
176
+ # if in_progress.success?
177
+ # # ...
178
+ # else
179
+ # # ...
180
+ # end
181
+ #
182
+ # Of course, you are free to ignore the returned Command, say if performance
183
+ # is more critical than getting a +message+ through.
184
+ #
185
+ def queue_for_mission(mission_id, message, options = { })
186
+ QueueCommand.new(mission_id, message, options)
187
+ end
188
+
189
+ #
190
+ # This method queues a report that will be sent straight to the Scout server
191
+ # during the next checkin. The passed +fields+ must be a Hash and must
192
+ # contain a <tt>:plugin_id</tt> key/value pair that tells the server which
193
+ # plugin this report belongs to. The returned object and background
194
+ # processing rules are the same as those described in queue_for_mission().
195
+ #
196
+ def queue_report(fields, options = { })
197
+ QueueCommand.new(:report, fields, options)
198
+ end
199
+
200
+ #
201
+ # This method queues a hint that will be sent straight to the Scout server
202
+ # during the next checkin. The passed +fields+ should follow the rules
203
+ # outlined in queue_report(). The returned object and background processing
204
+ # rules are the same as those described in queue_for_mission().
205
+ #
206
+ def queue_hint(fields, options = { })
207
+ QueueCommand.new(:hint, fields, options)
208
+ end
209
+
210
+ #
211
+ # This method queues an alert that will be sent straight to the Scout server
212
+ # during the next checkin. The passed +fields+ should follow the rules
213
+ # outlined in queue_report(). The returned object and background processing
214
+ # rules are the same as those described in queue_for_mission().
215
+ #
216
+ def queue_alert(fields, options = { })
217
+ QueueCommand.new(:alert, fields, options)
218
+ end
219
+
220
+ #
221
+ # This method queues an error that will be sent straight to the Scout server
222
+ # during the next checkin. The passed +fields+ should follow the rules
223
+ # outlined in queue_report(). The returned object and background processing
224
+ # rules are the same as those described in queue_for_mission().
225
+ #
226
+ def queue_error(fields, options = { })
227
+ QueueCommand.new(:error, fields, options)
228
+ end
229
+
230
+ #
231
+ # This method requests that the agent take a snapshot of the current
232
+ # environment, by running any commands the server has sent down. A
233
+ # timestamped result of these executions will be pushed up to the server
234
+ # during the next checkin. The returned object and background processing
235
+ # rules are the same as those described in queue_for_mission().
236
+ #
237
+ def take_snapshot(options = { })
238
+ Command.new(:snapshot, nil, nil, options)
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Assignment
5
+ { :plan => :file_and_switches,
6
+ :choose_user => false,
7
+ :choose_group => false }.each do |name, default|
8
+ instance_eval <<-END_CONFIG
9
+ def #{name}(setting = nil)
10
+ @#{name} ||= #{default.inspect} # default
11
+ @#{name} = setting if setting # writer
12
+ @#{name} # reader
13
+ end
14
+ END_CONFIG
15
+ end
16
+
17
+ def initialize(switches, other_args)
18
+ @switches = switches
19
+ @other_args = other_args
20
+ @user = nil
21
+ @group = nil
22
+ end
23
+
24
+ include Tracked
25
+
26
+ attr_reader :switches, :other_args, :user, :group
27
+
28
+ def prepare_and_execute
29
+ read_the_plan
30
+ choose_identity
31
+ execute
32
+ end
33
+
34
+ private
35
+
36
+ def read_the_plan
37
+ if self.class.plan.to_s.include? "file"
38
+ begin
39
+ Plan.update_from_config_file # load our config file
40
+ rescue Errno::ENOENT # missing config
41
+ abort_with_missing_config
42
+ end
43
+ end
44
+ if not @switches.empty? and self.class.plan.to_s.include? "switches"
45
+ Plan.update_from_switches(@switches) # override with switches
46
+ end
47
+ end
48
+
49
+ def choose_identity
50
+ [ %w[user getpwnam],
51
+ %w[group getgrnam] ].each do |type, interface|
52
+ if self.class.send("choose_#{type}")
53
+ match = nil
54
+ Array(Plan.send("#{type}_choices")).each do |name|
55
+ begin
56
+ match = Etc.send(interface, name)
57
+ break
58
+ rescue ArgumentError # not found
59
+ # try the next choice
60
+ end
61
+ end
62
+ if match
63
+ instance_variable_set("@#{type}", match)
64
+ else
65
+ abort_with_not_found(type)
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def test_server_connection(quiet = false)
72
+ unless quiet
73
+ print "Testing server connection: "
74
+ $stdout.flush
75
+ end
76
+ if Server.new.get_plan
77
+ puts "success." unless quiet
78
+ true
79
+ else
80
+ puts "failed." unless quiet
81
+ false
82
+ end
83
+ end
84
+
85
+ def abort_with_missing_config
86
+ abort <<-END_MISSING_CONFIG.trim
87
+ Unable to load configuration file. Please make sure you
88
+ have setup the daemon with this command:
89
+
90
+ sudo #{$PROGRAM_NAME} identify
91
+
92
+ END_MISSING_CONFIG
93
+ end
94
+
95
+ def abort_with_not_found(type)
96
+ choices = Plan.send("#{type}_choices")
97
+ config = "\n\n config.#{type}_choices = %w[#{choices.join(" ")}]\n\n"
98
+ abort <<-END_NOT_FOUND.word_wrap + config
99
+ I was unable to find a #{type} from the choice#{'s' if choices.size != 1}
100
+ #{choices.to_word_list("or")}. Please edit the following line in
101
+ #{Plan.config_file} to insert a valid group for your system:
102
+ END_NOT_FOUND
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Assignment
5
+ class Configuration < Assignment
6
+ def execute
7
+ puts "Configuration"
8
+ puts "============="
9
+ puts
10
+ settings = [:agent_key] + Plan.defaults.map { |name, _| name }
11
+ size = settings.map { |name| name.to_s.size }.max
12
+ settings.each do |name|
13
+ value = case v = Plan.send(name)
14
+ when Pathname
15
+ if name.to_s =~ /\Aos_/
16
+ v.relative_path_from(Plan.prefix_path).to_s.inspect
17
+ else
18
+ v.to_s.inspect
19
+ end
20
+ when Array
21
+ "%w[#{v.join(' ')}]"
22
+ else
23
+ v.inspect
24
+ end
25
+ puts "config.%-#{size}s = %s" % [name, value]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Assignment
5
+ class Identify < Assignment
6
+ plan :switches_only
7
+ choose_group true
8
+
9
+ def execute
10
+ puts "Identifying Your Agent"
11
+ puts "======================"
12
+ puts
13
+
14
+ %w[config_file db_dir log_dir].each do |path|
15
+ full = dir = Plan.send(path)
16
+ loop do
17
+ dir = dir.dirname
18
+ break if dir.exist?
19
+ end
20
+ unless dir.writable?
21
+ abort_with_insufficient_permissions(full)
22
+ end
23
+ end
24
+
25
+ unless Plan.config_file.exist?
26
+ print <<-END_KEY_DESCRIPTION.trim.to_question
27
+ I need your Agent Key displayed in the Agent Settings tab
28
+ to communicate with the server. It looks like:
29
+
30
+ a7349498-bec3-4ddf-963c-149a666433a4
31
+
32
+ Enter the Agent Key:
33
+ END_KEY_DESCRIPTION
34
+ Plan.agent_key = gets.to_s.strip
35
+ puts
36
+ if test_server_connection
37
+ puts
38
+ else
39
+ puts
40
+ abort_with_bad_key
41
+ end
42
+ end
43
+
44
+ puts "Saving identification..."
45
+ if Plan.write_default_config_file
46
+ puts "Identification file '#{Plan.config_file}' created."
47
+ else
48
+ if Plan.config_file.exist?
49
+ puts "Identification file '#{Plan.config_file}' exists. Skipped."
50
+ else
51
+ abort_with_insufficient_permissions(Plan.config_file)
52
+ end
53
+ end
54
+ %w[db_dir log_dir].each do |path|
55
+ dir = Plan.send(path)
56
+ if dir.exist?
57
+ puts "Directory '#{dir}' exists. Skipped."
58
+ elsif Plan.send("build_#{path}", group.gid)
59
+ puts "Directory '#{dir}' created."
60
+ else
61
+ abort_with_insufficient_permissions(dir)
62
+ end
63
+ if path == "db_dir"
64
+ %w[statuses queue snapshots].each do |name|
65
+ db = Database.path(name)
66
+ if db.exist?
67
+ puts "Database '#{db}' exists. Skipped."
68
+ elsif Plan.prepare_global_database(name)
69
+ puts "Database '#{db}' created."
70
+ else
71
+ abort_with_insufficient_permissions(db)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ puts "Done."
77
+ puts
78
+
79
+ puts <<-END_START_INSTRUCTIONS.trim
80
+ You are now identified. You can start the agent with:
81
+
82
+ sudo #{$PROGRAM_NAME} start
83
+
84
+ END_START_INSTRUCTIONS
85
+ end
86
+
87
+ private
88
+
89
+ def abort_with_insufficient_permissions(path)
90
+ abort <<-END_PRIVILEGES.trim
91
+ I don't have enough privileges to create '#{path}'.
92
+ Try running this program again with super user privileges:
93
+
94
+ sudo #{$PROGRAM_NAME} identify
95
+
96
+ END_PRIVILEGES
97
+ end
98
+
99
+ def abort_with_bad_key
100
+ abort <<-END_BAD_KEY.trim
101
+ Could not contact server. The key may be incorrect.
102
+ For more help, please visit:
103
+
104
+ http://scoutapp.com/help
105
+
106
+ END_BAD_KEY
107
+ end
108
+ end
109
+ end
110
+ end