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