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,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