scout_agent 3.0.6 → 3.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +18 -0
- data/Rakefile +1 -1
- data/lib/scout_agent/agent/communication_agent.rb +5 -1
- data/lib/scout_agent/agent/master_agent.rb +6 -6
- data/lib/scout_agent/agent.rb +26 -5
- data/lib/scout_agent/api.rb +52 -12
- data/lib/scout_agent/assignment/configuration.rb +11 -0
- data/lib/scout_agent/assignment/identify.rb +26 -0
- data/lib/scout_agent/assignment/queue.rb +35 -0
- data/lib/scout_agent/assignment/reset.rb +30 -0
- data/lib/scout_agent/assignment/snapshot.rb +50 -4
- data/lib/scout_agent/assignment/start.rb +65 -2
- data/lib/scout_agent/assignment/status.rb +22 -3
- data/lib/scout_agent/assignment/stop.rb +42 -5
- data/lib/scout_agent/assignment/test.rb +366 -0
- data/lib/scout_agent/assignment/update.rb +20 -0
- data/lib/scout_agent/assignment/upload_log.rb +31 -1
- data/lib/scout_agent/assignment.rb +92 -13
- data/lib/scout_agent/core_extensions.rb +24 -5
- data/lib/scout_agent/dispatcher.rb +45 -1
- data/lib/scout_agent/lifeline.rb +7 -2
- data/lib/scout_agent/mission.rb +31 -11
- data/lib/scout_agent/order/check_in_order.rb +27 -1
- data/lib/scout_agent/order/snapshot_order.rb +16 -0
- data/lib/scout_agent/order.rb +57 -11
- data/lib/scout_agent/plan.rb +1 -1
- data/lib/scout_agent.rb +1 -1
- metadata +13 -5
@@ -0,0 +1,366 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
# require standard libraries
|
5
|
+
require "open-uri"
|
6
|
+
require "yaml"
|
7
|
+
require "io/wait"
|
8
|
+
|
9
|
+
# load agent extensions
|
10
|
+
require "scout_agent/mission"
|
11
|
+
|
12
|
+
# shut off SSL verification by open-uri
|
13
|
+
module OpenSSL::SSL
|
14
|
+
remove_const(:VERIFY_PEER)
|
15
|
+
VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
|
16
|
+
end
|
17
|
+
|
18
|
+
module ScoutAgent
|
19
|
+
class Assignment
|
20
|
+
#
|
21
|
+
# Invoke with:
|
22
|
+
#
|
23
|
+
# scout_agent test [once] path/to/plugin.rb [<<< '{"options": "in JSON"}']
|
24
|
+
#
|
25
|
+
# This command is a test mode for plugin developers. It allows you to run a
|
26
|
+
# plugin and see what that code generates.
|
27
|
+
#
|
28
|
+
# The only required argument is a path to the plugin you wish to test. That
|
29
|
+
# path can be a file path or a URL to load the code from.
|
30
|
+
#
|
31
|
+
# By default, URL's will just be run once and files will be run in a loop
|
32
|
+
# that watches the code for changes and reruns until you control-c the
|
33
|
+
# process. You can add the once parameter to have a file treated as a URL.
|
34
|
+
#
|
35
|
+
# Default options will be read from a matching .yml file at the same path,
|
36
|
+
# if available. You can also provide option settings on <tt>$stdin</tt>
|
37
|
+
# which will override the defaults.
|
38
|
+
#
|
39
|
+
# If you need to test a plugin that expects queued messages, pass a
|
40
|
+
# <tt>"__queue__"</tt> option that's an Array of messages.
|
41
|
+
# They will be delivered to your plugin.
|
42
|
+
#
|
43
|
+
class Test < Assignment
|
44
|
+
# Runs the test command.
|
45
|
+
def execute
|
46
|
+
install_interrupt_handler
|
47
|
+
prepare_log
|
48
|
+
parse_options
|
49
|
+
prepare_mission
|
50
|
+
load_code
|
51
|
+
load_options
|
52
|
+
test_mission
|
53
|
+
end
|
54
|
+
|
55
|
+
#######
|
56
|
+
private
|
57
|
+
#######
|
58
|
+
|
59
|
+
############
|
60
|
+
### Test ###
|
61
|
+
############
|
62
|
+
|
63
|
+
# Install a nice shutdown that doesn't include an Exception backtrace.
|
64
|
+
def install_interrupt_handler
|
65
|
+
trap("INT") do
|
66
|
+
Thread.new do
|
67
|
+
@log.info("Stopping.") if @log
|
68
|
+
exit
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Builds a simple log to <tt>$stdout</tt> that shows all messages.
|
74
|
+
def prepare_log
|
75
|
+
@log = WireTap.new($stdout)
|
76
|
+
@log.progname = :test
|
77
|
+
end
|
78
|
+
|
79
|
+
# Pulls out the optional run-once and the required path options.
|
80
|
+
def parse_options
|
81
|
+
args = Array(other_args)
|
82
|
+
|
83
|
+
# check for once option
|
84
|
+
@once = false
|
85
|
+
@url_mode = false
|
86
|
+
if args.first == "once"
|
87
|
+
@log.info("Starting single plugin run.")
|
88
|
+
@once = args.shift
|
89
|
+
elsif args.first.to_s =~ %r{\A[A-Za-z][-A-Za-z0-9+.]*://}
|
90
|
+
@log.info("Testing plugin from a URL.")
|
91
|
+
@once = @url_mode = true
|
92
|
+
else
|
93
|
+
@log.info( "Starting plugin development mode. " +
|
94
|
+
"Watching code for changes." )
|
95
|
+
end
|
96
|
+
|
97
|
+
# get the code path
|
98
|
+
unless @mission_path = args.shift
|
99
|
+
abort_with_missing_path
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Build the mission details that would normally have come from the
|
105
|
+
# database.
|
106
|
+
#
|
107
|
+
def prepare_mission
|
108
|
+
@mission = { :id => :testing,
|
109
|
+
:name => File.basename(@mission_path, ".rb"),
|
110
|
+
:last_run_at => nil,
|
111
|
+
:memory => Hash.new,
|
112
|
+
:options => Hash.new }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Reads the code from disk or URL, loading it into the mission details.
|
116
|
+
def load_code
|
117
|
+
if @url_mode or ( File.exist?(@mission_path) and
|
118
|
+
File.file?(@mission_path) and
|
119
|
+
File.readable?(@mission_path) )
|
120
|
+
begin
|
121
|
+
@mission[:code] = read_file_or_url(@mission_path)
|
122
|
+
rescue Exception # reading error
|
123
|
+
abort_with_unreadable(@mission_path)
|
124
|
+
end
|
125
|
+
else
|
126
|
+
abort_with_unreadable(@mission_path)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Reads the options first from the defaults file and then as overrides
|
132
|
+
# from <tt>$stdin</tt>.
|
133
|
+
#
|
134
|
+
def load_options
|
135
|
+
# look for default options in the standard place
|
136
|
+
@options_path = @mission_path.sub(/\.rb/i, ".yml")
|
137
|
+
if @url_mode or ( File.exist?(@options_path) and
|
138
|
+
File.file?(@options_path) and
|
139
|
+
File.readable?(@options_path) )
|
140
|
+
@log.info("Loading default options from '#{@options_path}'.")
|
141
|
+
begin
|
142
|
+
defaults = YAML.load(read_file_or_url(@options_path))
|
143
|
+
rescue Exception => error
|
144
|
+
if @url_mode
|
145
|
+
@log.warn("Options not found at '#{@options_path}'.")
|
146
|
+
else
|
147
|
+
case error
|
148
|
+
when ArgumentError # invalid YAML
|
149
|
+
abort_with_bad_defaults
|
150
|
+
else # reading error
|
151
|
+
abort_with_unreadable(@options_path)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
if defaults.is_a?(Hash) and defaults["options"].is_a?(Hash)
|
156
|
+
defaults["options"].each do |name, details|
|
157
|
+
if details.is_a? Hash
|
158
|
+
@mission[:options].merge!(name => details["default"])
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
else
|
163
|
+
@log.warn("Options file '#{@options_path}' not found.")
|
164
|
+
end
|
165
|
+
|
166
|
+
# also check for overrides
|
167
|
+
if defined?(@overrides) or $stdin.ready?
|
168
|
+
@log.info("Reading and setting option overrides.")
|
169
|
+
begin
|
170
|
+
@overrides ||= JSON.parse($stdin.read)
|
171
|
+
rescue JSON::ParserError
|
172
|
+
abort_with_bad_overrides
|
173
|
+
end
|
174
|
+
if @overrides.is_a? Hash
|
175
|
+
@mission[:options].merge!(@overrides)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# The main testing event loop for a plugin. This controls execution,
|
182
|
+
# waiting through the run, updating time and memory, and waiting for
|
183
|
+
# code changes.
|
184
|
+
#
|
185
|
+
def test_mission
|
186
|
+
unless @once
|
187
|
+
code_last_modified = File.stat(@mission_path).mtime
|
188
|
+
options_last_modified = File.stat(@options_path).mtime rescue nil
|
189
|
+
end
|
190
|
+
loop do
|
191
|
+
# fork off the plugin process
|
192
|
+
@reader, @writer = IO.pipe
|
193
|
+
mission_pid = fork do
|
194
|
+
reset_environment
|
195
|
+
compile_mission
|
196
|
+
run_mission
|
197
|
+
complete_mission
|
198
|
+
end
|
199
|
+
|
200
|
+
# ensure the child process exits in reasonable time
|
201
|
+
@writer.close
|
202
|
+
exit_status = nil
|
203
|
+
begin
|
204
|
+
Timeout.timeout(60) do
|
205
|
+
exit_status = Process.wait2(mission_pid).last
|
206
|
+
end
|
207
|
+
rescue Timeout::Error # mission exceeded allowed execution
|
208
|
+
exit_status = Process.term_or_kill(mission_pid)
|
209
|
+
@log.error( "#{@mission[:name]} took too long to run: " +
|
210
|
+
"#{exit_status && exit_status.exitstatus}." )
|
211
|
+
end
|
212
|
+
|
213
|
+
# stop there if this is a one shot
|
214
|
+
break if @once
|
215
|
+
|
216
|
+
# update mission details
|
217
|
+
@mission[:last_run_at] = Time.now
|
218
|
+
if exit_status and exit_status.success?
|
219
|
+
begin
|
220
|
+
@mission[:memory] = JSON.parse(@reader.read)
|
221
|
+
@log.debug("Plugin memory updated to %p." % @mission[:memory])
|
222
|
+
rescue Exception # bad memory transfer
|
223
|
+
@log.error("Memory could not be updated.")
|
224
|
+
# do nothng: keeping old memory
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# reload the code when it changes
|
229
|
+
@log.info("Watching plugin code for changes.")
|
230
|
+
loop do
|
231
|
+
modified = false
|
232
|
+
code_modified = File.stat(@mission_path).mtime
|
233
|
+
if code_modified > code_last_modified
|
234
|
+
code_last_modified = code_modified
|
235
|
+
load_code
|
236
|
+
modified = true
|
237
|
+
end
|
238
|
+
options_modified = File.stat(@options_path).mtime rescue nil
|
239
|
+
if (not options_last_modified and options_modified) or
|
240
|
+
( options_last_modified and
|
241
|
+
options_modified and
|
242
|
+
options_modified > options_last_modified )
|
243
|
+
options_last_modified = options_modified
|
244
|
+
load_options
|
245
|
+
modified = true
|
246
|
+
end
|
247
|
+
break if modified
|
248
|
+
sleep 1
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
###############
|
254
|
+
### Helpers ###
|
255
|
+
###############
|
256
|
+
|
257
|
+
# Loads data from a file or URL +path+, without OpenSSL warnings.
|
258
|
+
def read_file_or_url(path)
|
259
|
+
no_warnings { open(path) { |data| data.read } }
|
260
|
+
end
|
261
|
+
|
262
|
+
###############
|
263
|
+
### Mission ###
|
264
|
+
###############
|
265
|
+
|
266
|
+
# Resets the log process name and closes have of the memory transfer pipe.
|
267
|
+
def reset_environment
|
268
|
+
@log = WireTap.new($stdout)
|
269
|
+
@log.progname = :mission
|
270
|
+
|
271
|
+
@reader.close
|
272
|
+
end
|
273
|
+
|
274
|
+
# Compiles the mission code without generating database errors.
|
275
|
+
def compile_mission
|
276
|
+
@log.info("Compiling #{@mission[:name]} mission.")
|
277
|
+
begin
|
278
|
+
eval(@mission[:code], TOPLEVEL_BINDING, @mission[:name])
|
279
|
+
rescue Exception => error # any compile error
|
280
|
+
raise if $!.is_a? SystemExit # don't catch exit() calls
|
281
|
+
@log.error( "#{@mission[:name]} could not be compiled: " +
|
282
|
+
"#{error.message} (#{error.class})." )
|
283
|
+
exit(2) # warn parent that we failed to compile
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Executes the mission without generating database errors.
|
288
|
+
def run_mission
|
289
|
+
@log.info("Preparing #{@mission[:name]} mission.")
|
290
|
+
if prepared = Mission.prepared
|
291
|
+
@log.info("Starting #{@mission[:name]} mission.")
|
292
|
+
@mission[:object] =
|
293
|
+
prepared.new( *( @mission.values_at( :id,
|
294
|
+
:name,
|
295
|
+
:last_run_at,
|
296
|
+
:memory,
|
297
|
+
:options ) + [@log] ) )
|
298
|
+
@mission[:object].run
|
299
|
+
else # no mission loaded
|
300
|
+
@log.error("#{@mission[:name]} could not be prepared.")
|
301
|
+
exit(3) # warn parent that we couldn't prpare mission
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Transfers mission memory back to the testing process.
|
306
|
+
def complete_mission
|
307
|
+
@log.info("#{@mission[:name]} mission complete.")
|
308
|
+
@writer.puts @mission[:object].data_for_server[:memory].to_json
|
309
|
+
end
|
310
|
+
|
311
|
+
##############
|
312
|
+
### Errors ###
|
313
|
+
##############
|
314
|
+
|
315
|
+
#
|
316
|
+
# Abort with an error message to the user that explains that the path or
|
317
|
+
# URL is a required argument.
|
318
|
+
#
|
319
|
+
def abort_with_missing_path
|
320
|
+
abort <<-END_MISSING_PATH.trim
|
321
|
+
|
322
|
+
You must provide a path to your code on the file system or a URL to
|
323
|
+
fetch it from:
|
324
|
+
|
325
|
+
scout_agent test PATH_OR_URL
|
326
|
+
|
327
|
+
END_MISSING_PATH
|
328
|
+
end
|
329
|
+
|
330
|
+
#
|
331
|
+
# Abort with an error message to the user that says we can't parse the
|
332
|
+
# provided JSON.
|
333
|
+
#
|
334
|
+
def abort_with_unreadable(file)
|
335
|
+
abort "\nFailed to read from '#{file}'."
|
336
|
+
end
|
337
|
+
|
338
|
+
#
|
339
|
+
# Abort with an error message to the user that says we can't parse the
|
340
|
+
# default options YAML file.
|
341
|
+
#
|
342
|
+
def abort_with_bad_defaults
|
343
|
+
abort <<-END_BAD_DEFAULTS.trim
|
344
|
+
|
345
|
+
Failed to parse option defaults. You must provide default options in
|
346
|
+
Scout's standard YAML format.
|
347
|
+
END_BAD_DEFAULTS
|
348
|
+
end
|
349
|
+
|
350
|
+
#
|
351
|
+
# Abort with an error message to the user that says we can't parse the
|
352
|
+
# provided JSON.
|
353
|
+
#
|
354
|
+
def abort_with_bad_overrides
|
355
|
+
abort <<-END_BAD_OVERRIDES.trim
|
356
|
+
|
357
|
+
Failed to parse option overrides. You must provide options in valid
|
358
|
+
JSON. For example:
|
359
|
+
|
360
|
+
scout_agent test … <<< '{"field": "setting", "num": 42}'
|
361
|
+
|
362
|
+
END_BAD_OVERRIDES
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
@@ -3,7 +3,19 @@
|
|
3
3
|
|
4
4
|
module ScoutAgent
|
5
5
|
class Assignment
|
6
|
+
#
|
7
|
+
# Invoke with:
|
8
|
+
#
|
9
|
+
# sudo scout_agent update
|
10
|
+
#
|
11
|
+
# This command rewrites the current configuration file. As such, it has two
|
12
|
+
# main uses. First, you can run this command to load the current file, pass
|
13
|
+
# some switches to change settings, and it will save the changes like a text
|
14
|
+
# editor would. It's also useful when an updated version of the agent
|
15
|
+
# includes new options you would like to have added to your existing file.
|
16
|
+
#
|
6
17
|
class Update < Assignment
|
18
|
+
# Runs the update command.
|
7
19
|
def execute
|
8
20
|
begin
|
9
21
|
Plan.config_file.unlink
|
@@ -22,6 +34,14 @@ module ScoutAgent
|
|
22
34
|
end
|
23
35
|
end
|
24
36
|
|
37
|
+
#######
|
38
|
+
private
|
39
|
+
#######
|
40
|
+
|
41
|
+
#
|
42
|
+
# Abort with an error message to the user that says we don't have enough
|
43
|
+
# permission to rewrite the configuration file.
|
44
|
+
#
|
25
45
|
def abort_with_sudo_required
|
26
46
|
abort <<-END_SUDO_REQUIRED.trim
|
27
47
|
I don't have enough privileges to update your identity and
|
@@ -6,18 +6,34 @@ require "tempfile"
|
|
6
6
|
|
7
7
|
module ScoutAgent
|
8
8
|
class Assignment
|
9
|
+
#
|
10
|
+
# Invoke with:
|
11
|
+
#
|
12
|
+
# scout_agent upload_log [FILE_DATE]
|
13
|
+
#
|
14
|
+
# This command can help us troubleshoot problems with your agent. You can
|
15
|
+
# invoke this to pass a log file up to our servers, so we can look through
|
16
|
+
# it for problems. Note that this is never done automatically. You must
|
17
|
+
# invoke this command manually.
|
18
|
+
#
|
19
|
+
# The +FILE_DATE+ is just the date of the log file in the form YYYY-MM-DD.
|
20
|
+
#
|
9
21
|
class UploadLog < Assignment
|
22
|
+
# Runs the upload_log command.
|
10
23
|
def execute
|
24
|
+
# build the log file name
|
11
25
|
file_name = "#{ScoutAgent.agent_name}.log"
|
12
26
|
if date = Array(other_args).shift
|
13
27
|
file_name += ".#{date.delete('^0-9')}"
|
14
28
|
end
|
15
29
|
log_file = Plan.log_dir + file_name
|
16
30
|
|
31
|
+
# ensure it exists
|
17
32
|
unless log_file.exist?
|
18
33
|
abort_with_not_found(file_name)
|
19
34
|
end
|
20
35
|
|
36
|
+
# copy the log to a zipped temporary file
|
21
37
|
puts "Preparing file for the server. This may take a moment..."
|
22
38
|
begin
|
23
39
|
upload_file = Tempfile.new("#{file_name}.gz")
|
@@ -32,7 +48,7 @@ module ScoutAgent
|
|
32
48
|
end
|
33
49
|
puts "Done."
|
34
50
|
|
35
|
-
|
51
|
+
# upload the zipped temporary file to the server
|
36
52
|
puts "Sending file to the server. This may take a moment..."
|
37
53
|
server = Server.new
|
38
54
|
if server.post_log(upload_file)
|
@@ -42,18 +58,32 @@ module ScoutAgent
|
|
42
58
|
end
|
43
59
|
end
|
44
60
|
|
61
|
+
#######
|
45
62
|
private
|
63
|
+
#######
|
46
64
|
|
65
|
+
#
|
66
|
+
# Abort with an error message to the user that says we can't find the
|
67
|
+
# indicated or default log file.
|
68
|
+
#
|
47
69
|
def abort_with_not_found(log_file_name)
|
48
70
|
abort "'#{log_file_name}' could not be found to upload."
|
49
71
|
end
|
50
72
|
|
73
|
+
#
|
74
|
+
# Abort with an error message to the user that says why we can't prepare
|
75
|
+
# the file for upload.
|
76
|
+
#
|
51
77
|
def abort_with_preparation_error(error)
|
52
78
|
abort <<-END_PREPARATION_ERROR
|
53
79
|
Could not prepare file for upload: #{error.message} (#{error.class})
|
54
80
|
END_PREPARATION_ERROR
|
55
81
|
end
|
56
82
|
|
83
|
+
#
|
84
|
+
# Abort with an error message to the user that says we received a server
|
85
|
+
# error when we attempted to upload.
|
86
|
+
#
|
57
87
|
def abort_with_server_error
|
58
88
|
abort "A networking error prevented the file from being sent."
|
59
89
|
end
|
@@ -2,19 +2,62 @@
|
|
2
2
|
# encoding: UTF-8
|
3
3
|
|
4
4
|
module ScoutAgent
|
5
|
+
#
|
6
|
+
# An Assignment is a command that can be given to the agent on the
|
7
|
+
# command-line. This object encapsulates the series of steps needed to invoke
|
8
|
+
# a command and subclasses supply the specific behavior.
|
9
|
+
#
|
5
10
|
class Assignment
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
#
|
12
|
+
# This method is a factory for one method class attributes supporting
|
13
|
+
# getting (with a default) and setting.
|
14
|
+
#
|
15
|
+
def self.get_or_set(name, default, setting = nil)
|
16
|
+
var = "@#{name}"
|
17
|
+
if setting
|
18
|
+
instance_variable_set(var, setting) # writer
|
19
|
+
elsif not instance_variable_defined? var
|
20
|
+
instance_variable_set(var, default) # default
|
14
21
|
end
|
15
|
-
|
22
|
+
instance_variable_get(var) # reader
|
16
23
|
end
|
24
|
+
private_class_method :get_or_set
|
17
25
|
|
26
|
+
#
|
27
|
+
# If the +setting+ of this attribute includes the word "file", the
|
28
|
+
# configuration file will be loaded before the command is invoked.
|
29
|
+
# Similarly, the word "switches" will cause the configuration to be updated
|
30
|
+
# using the provided command-line switches. The default setting is
|
31
|
+
# <tt>:file_and_switches</tt>.
|
32
|
+
#
|
33
|
+
def self.plan(setting = nil)
|
34
|
+
get_or_set(:plan, :file_and_switches, setting)
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# If +setting+ is +true+, a user to operate as will be selected based on the
|
39
|
+
# configuration. Note that a user is just selected and it's up to the
|
40
|
+
# command to make use of it. That user is available to commands via the
|
41
|
+
# user() method.
|
42
|
+
#
|
43
|
+
def self.choose_user(setting = nil)
|
44
|
+
get_or_set(:choose_user, false, setting)
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# If +setting+ is +true+, a group to operate as will be selected based on
|
49
|
+
# the configuration. Note that a group is just selected and it's up to the
|
50
|
+
# command to make use of it. That group is available to commands via the
|
51
|
+
# group() method.
|
52
|
+
#
|
53
|
+
def self.choose_group(setting = nil)
|
54
|
+
get_or_set(:choose_group, false, setting)
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Builds a new Assignment with the passed +switches+ and +other_args+. The
|
59
|
+
# Dispatcher usess this to prepare a command for running.
|
60
|
+
#
|
18
61
|
def initialize(switches, other_args)
|
19
62
|
@switches = switches
|
20
63
|
@other_args = other_args
|
@@ -24,16 +67,35 @@ module ScoutAgent
|
|
24
67
|
|
25
68
|
include Tracked
|
26
69
|
|
27
|
-
|
70
|
+
# The command-line switches passed to the command.
|
71
|
+
attr_reader :switches
|
72
|
+
# Any non-switch arguments passed to the command.
|
73
|
+
attr_reader :other_args
|
74
|
+
# The user to operate as, if selected. See choose_user() for details.
|
75
|
+
attr_reader :user
|
76
|
+
# The group to operate as, if selected. See choose_group() for details.
|
77
|
+
attr_reader :group
|
28
78
|
|
79
|
+
#
|
80
|
+
# Loads configuaration as directed by plan(), selects indentity as directed
|
81
|
+
# by choose_user() and choose_group(), then calls execute() for the command.
|
82
|
+
# Subclasses provide the execute() method to provide their specific
|
83
|
+
# behavior. The Dispatcher calls this method to launch a command.
|
84
|
+
#
|
29
85
|
def prepare_and_execute
|
30
86
|
read_the_plan
|
31
87
|
choose_identity
|
32
88
|
execute
|
33
89
|
end
|
34
90
|
|
91
|
+
#######
|
35
92
|
private
|
93
|
+
#######
|
36
94
|
|
95
|
+
#
|
96
|
+
# Reads the configuration file and/or updates the plan from the provided
|
97
|
+
# command-line switches. See the plan() method for details.
|
98
|
+
#
|
37
99
|
def read_the_plan
|
38
100
|
if self.class.plan.to_s.include? "file"
|
39
101
|
begin
|
@@ -47,6 +109,10 @@ module ScoutAgent
|
|
47
109
|
end
|
48
110
|
end
|
49
111
|
|
112
|
+
#
|
113
|
+
# Selects a user and/or group as dictated by choose_user() and
|
114
|
+
# choose_group(). See those methods for details.
|
115
|
+
#
|
50
116
|
def choose_identity
|
51
117
|
[ %w[user getpwnam],
|
52
118
|
%w[group getgrnam] ].each do |type, interface|
|
@@ -69,20 +135,29 @@ module ScoutAgent
|
|
69
135
|
end
|
70
136
|
end
|
71
137
|
|
138
|
+
#
|
139
|
+
# Attempts to fetch a plan from the server as a way to test connectivity and
|
140
|
+
# the agent settings. A status line will be printed to <tt>$stdout</tt>
|
141
|
+
# unless +quiet+ is +true+.
|
142
|
+
#
|
72
143
|
def test_server_connection(quiet = false)
|
73
144
|
unless quiet
|
74
|
-
print "Testing server connection: "
|
145
|
+
$stdout.print "Testing server connection: "
|
75
146
|
$stdout.flush
|
76
147
|
end
|
77
148
|
if Server.new.get_plan
|
78
|
-
puts "success." unless quiet
|
149
|
+
$stdout.puts "success." unless quiet
|
79
150
|
true
|
80
151
|
else
|
81
|
-
puts "failed." unless quiet
|
152
|
+
$stdout.puts "failed." unless quiet
|
82
153
|
false
|
83
154
|
end
|
84
155
|
end
|
85
156
|
|
157
|
+
#
|
158
|
+
# Abort with an error message to the user that says we cannot load their
|
159
|
+
# configuration file.
|
160
|
+
#
|
86
161
|
def abort_with_missing_config
|
87
162
|
abort <<-END_MISSING_CONFIG.trim
|
88
163
|
Unable to load configuration file. Please make sure you
|
@@ -93,6 +168,10 @@ module ScoutAgent
|
|
93
168
|
END_MISSING_CONFIG
|
94
169
|
end
|
95
170
|
|
171
|
+
#
|
172
|
+
# Abort with an error message to the user that says we cannot select a
|
173
|
+
# suitable <tt>:user</tt> or <tt>:group</tt> to operate as, based on +type+.
|
174
|
+
#
|
96
175
|
def abort_with_not_found(type)
|
97
176
|
choices = Plan.send("#{type}_choices")
|
98
177
|
config = "\n\n config.#{type}_choices = %w[#{choices.join(" ")}]\n\n"
|
@@ -161,8 +161,8 @@ module ScoutAgent
|
|
161
161
|
%w[TERM KILL].each { |signal|
|
162
162
|
begin
|
163
163
|
::Process.kill(signal, child_pid) # attempt to stop process
|
164
|
-
rescue
|
165
|
-
break
|
164
|
+
rescue Exception # no such process
|
165
|
+
break # the process is stopped
|
166
166
|
end
|
167
167
|
if signal == "TERM"
|
168
168
|
# give them a chance to respond
|
@@ -170,7 +170,8 @@ module ScoutAgent
|
|
170
170
|
Timeout.timeout(pause) {
|
171
171
|
begin
|
172
172
|
return ::Process.wait2(child_pid).last # response to signal
|
173
|
-
rescue
|
173
|
+
rescue Exception => error # no such child
|
174
|
+
raise if error.is_a? Timeout::Error # reraise timeouts
|
174
175
|
return nil # we have already caught the child
|
175
176
|
end
|
176
177
|
}
|
@@ -181,8 +182,8 @@ module ScoutAgent
|
|
181
182
|
}
|
182
183
|
begin
|
183
184
|
::Process.wait2(child_pid).last
|
184
|
-
rescue
|
185
|
-
nil
|
185
|
+
rescue Exception # no such child
|
186
|
+
nil # we have already caught the child
|
186
187
|
end
|
187
188
|
end
|
188
189
|
end
|
@@ -271,6 +272,15 @@ module ScoutAgent
|
|
271
272
|
::Time.local(*str.to_s.scan(/\d+/))
|
272
273
|
end
|
273
274
|
end
|
275
|
+
|
276
|
+
#
|
277
|
+
# Half of the support for the round-tripping of Time objects to and from
|
278
|
+
# JSON. This method reconstructs a Time object from a raw JSON object
|
279
|
+
# representation built by our to_json() override.
|
280
|
+
#
|
281
|
+
def json_create(object)
|
282
|
+
::Time.local(*object["data"])
|
283
|
+
end
|
274
284
|
end
|
275
285
|
|
276
286
|
# Extensions for the Time class.
|
@@ -283,6 +293,15 @@ module ScoutAgent
|
|
283
293
|
def to_db_s(trim_seconds = false)
|
284
294
|
strftime("%Y-%m-%d %H:%M#{':%S' unless trim_seconds}")
|
285
295
|
end
|
296
|
+
|
297
|
+
#
|
298
|
+
# Half of the support for the round-tripping of Time objects to and from
|
299
|
+
# JSON. This method builds a JSON object representation for Time objects
|
300
|
+
# compatible with the JSON library's Ruby conversion support.
|
301
|
+
#
|
302
|
+
def to_json(*args)
|
303
|
+
{:json_class => self.class.name, :data => to_a}.to_json(*args)
|
304
|
+
end
|
286
305
|
end
|
287
306
|
end
|
288
307
|
end
|