scout_agent 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +4 -0
- data/CHANGELOG +3 -0
- data/COPYING +340 -0
- data/INSTALL +17 -0
- data/LICENSE +6 -0
- data/README +3 -0
- data/Rakefile +123 -0
- data/TODO +3 -0
- data/bin/scout_agent +11 -0
- data/lib/scout_agent.rb +73 -0
- data/lib/scout_agent/agent.rb +42 -0
- data/lib/scout_agent/agent/communication_agent.rb +85 -0
- data/lib/scout_agent/agent/master_agent.rb +301 -0
- data/lib/scout_agent/api.rb +241 -0
- data/lib/scout_agent/assignment.rb +105 -0
- data/lib/scout_agent/assignment/configuration.rb +30 -0
- data/lib/scout_agent/assignment/identify.rb +110 -0
- data/lib/scout_agent/assignment/queue.rb +95 -0
- data/lib/scout_agent/assignment/reset.rb +91 -0
- data/lib/scout_agent/assignment/snapshot.rb +92 -0
- data/lib/scout_agent/assignment/start.rb +149 -0
- data/lib/scout_agent/assignment/status.rb +44 -0
- data/lib/scout_agent/assignment/stop.rb +60 -0
- data/lib/scout_agent/assignment/upload_log.rb +61 -0
- data/lib/scout_agent/core_extensions.rb +260 -0
- data/lib/scout_agent/database.rb +386 -0
- data/lib/scout_agent/database/mission_log.rb +282 -0
- data/lib/scout_agent/database/queue.rb +126 -0
- data/lib/scout_agent/database/snapshots.rb +187 -0
- data/lib/scout_agent/database/statuses.rb +65 -0
- data/lib/scout_agent/dispatcher.rb +157 -0
- data/lib/scout_agent/id_card.rb +143 -0
- data/lib/scout_agent/lifeline.rb +243 -0
- data/lib/scout_agent/mission.rb +212 -0
- data/lib/scout_agent/order.rb +58 -0
- data/lib/scout_agent/order/check_in_order.rb +32 -0
- data/lib/scout_agent/order/snapshot_order.rb +33 -0
- data/lib/scout_agent/plan.rb +306 -0
- data/lib/scout_agent/server.rb +123 -0
- data/lib/scout_agent/tracked.rb +59 -0
- data/lib/scout_agent/wire_tap.rb +513 -0
- data/setup.rb +1360 -0
- data/test/tc_core_extensions.rb +89 -0
- data/test/tc_id_card.rb +115 -0
- data/test/tc_plan.rb +285 -0
- data/test/test_helper.rb +22 -0
- data/test/ts_all.rb +7 -0
- 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
|