scout_agent 3.0.6 → 3.0.7
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/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
|