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,95 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
# require standard libraries
|
4
|
+
require "io/wait"
|
5
|
+
|
6
|
+
module ScoutAgent
|
7
|
+
class Assignment
|
8
|
+
class Queue < Assignment
|
9
|
+
ERRORS = [ [ :missing_db,
|
10
|
+
"Queue database could not be loaded." ],
|
11
|
+
[ :missing_id,
|
12
|
+
"You must pass a mission ID or delivery type." ],
|
13
|
+
[ :invalid_id,
|
14
|
+
"You must pass a mission ID or " +
|
15
|
+
"'report', 'hint', 'alert', or 'error'." ],
|
16
|
+
[ :missing_fields,
|
17
|
+
"You must provide fields to queue." ],
|
18
|
+
[ :invalid_fields,
|
19
|
+
"You must pass valid JSON for the fields." ],
|
20
|
+
[ :invalid_report_fields,
|
21
|
+
"Field data must be a Hash to pass to the server." ],
|
22
|
+
[ :missing_plugin_id_field,
|
23
|
+
"A plugin_id field is required by the server." ],
|
24
|
+
[ :invalid_plugin_id_field,
|
25
|
+
"The plugin_id field must be a positive integer." ],
|
26
|
+
[ :failed_to_queue_message,
|
27
|
+
"Your message could not be queued at this time." ] ]
|
28
|
+
|
29
|
+
def execute
|
30
|
+
log = ScoutAgent.prepare_wire_tap(:queue, :skip_stdout)
|
31
|
+
|
32
|
+
status_database(log)
|
33
|
+
status("Queuing message", :queue)
|
34
|
+
at_my_exit do
|
35
|
+
clear_status(:queue)
|
36
|
+
end
|
37
|
+
|
38
|
+
unless db = Database.load(:queue, log)
|
39
|
+
abort_with_error(:missing_db)
|
40
|
+
end
|
41
|
+
|
42
|
+
log.info("Validating message for queuing.")
|
43
|
+
unless id = Array(other_args).shift
|
44
|
+
abort_with_error(:missing_id)
|
45
|
+
end
|
46
|
+
unless id =~ /\A(?:report|hint|alert|error|\d*[1-9])\z/
|
47
|
+
abort_with_error(:invalid_id)
|
48
|
+
end
|
49
|
+
|
50
|
+
fields = ARGF.read unless ARGV.empty? and not $stdin.ready?
|
51
|
+
if fields.nil? or fields.empty?
|
52
|
+
abort_with_error(:missing_fields)
|
53
|
+
end
|
54
|
+
bytes = fields.size
|
55
|
+
begin
|
56
|
+
fields = JSON.parse(fields.to_s)
|
57
|
+
rescue JSON::ParserError
|
58
|
+
abort_with_error(:invalid_fields)
|
59
|
+
end
|
60
|
+
|
61
|
+
if %w[report hint alert error].include? id
|
62
|
+
unless fields.is_a? Hash
|
63
|
+
abort_with_error(:invalid_report_fields)
|
64
|
+
end
|
65
|
+
unless fields.include? "plugin_id"
|
66
|
+
abort_with_error(:missing_plugin_id_field)
|
67
|
+
end
|
68
|
+
unless fields["plugin_id"].to_s =~ /\A\d*[1-9]\z/
|
69
|
+
abort_with_error(:invalid_plugin_id_field)
|
70
|
+
end
|
71
|
+
log.info("Message is a valid #{id} (#{bytes} bytes).")
|
72
|
+
else
|
73
|
+
log.info("Message is valid (#{bytes} bytes).")
|
74
|
+
end
|
75
|
+
|
76
|
+
log.info("Queuing message.")
|
77
|
+
unless db.enqueue(id, fields)
|
78
|
+
abort_with_error(:failed_to_queue_message)
|
79
|
+
end
|
80
|
+
|
81
|
+
db.maintain
|
82
|
+
|
83
|
+
log.info("Messages queued successfully.")
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def abort_with_error(error_name)
|
89
|
+
error = ERRORS.assoc(error_name)
|
90
|
+
warn error.last
|
91
|
+
exit(ERRORS.index(error) + 1)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
module ScoutAgent
|
4
|
+
class Assignment
|
5
|
+
class Reset < Assignment
|
6
|
+
def execute
|
7
|
+
agent = IDCard.new(:lifeline)
|
8
|
+
if agent.pid_file.exist?
|
9
|
+
abort_with_agent_running
|
10
|
+
end
|
11
|
+
|
12
|
+
puts "Reset Your Agent"
|
13
|
+
puts "================"
|
14
|
+
puts
|
15
|
+
|
16
|
+
print <<-END_WARNING.trim.to_question
|
17
|
+
This is a dangerous operation that will remove configuration
|
18
|
+
and data files. Are you absolutely sure you wish to remove
|
19
|
+
this data (yes/no)?
|
20
|
+
END_WARNING
|
21
|
+
unless gets.to_s.strip.downcase == "yes"
|
22
|
+
abort_with_cancel
|
23
|
+
end
|
24
|
+
|
25
|
+
puts
|
26
|
+
puts "Resetting..."
|
27
|
+
if not Plan.config_file.exist?
|
28
|
+
puts <<-END_CONFIG_FILE
|
29
|
+
Identification file '#{Plan.config_file}' doesn't exist. Skipped.
|
30
|
+
END_CONFIG_FILE
|
31
|
+
else
|
32
|
+
begin
|
33
|
+
Plan.config_file.unlink
|
34
|
+
puts "Identification file '#{Plan.config_file}' removed."
|
35
|
+
rescue Errno::EACCES # don't have permission
|
36
|
+
abort_with_insufficient_permissions(Plan.config_file)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
%w[db_dir pid_dir log_dir].each do |path|
|
40
|
+
dir = Plan.send(path)
|
41
|
+
if not dir.exist?
|
42
|
+
puts "Directory '#{dir}' doesn't exist. Skipped."
|
43
|
+
else
|
44
|
+
begin
|
45
|
+
dir.rmtree
|
46
|
+
puts "Directory '#{dir}' removed."
|
47
|
+
rescue Errno::EACCES # don't have permission
|
48
|
+
abort_with_insufficient_permissions(dir)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
puts "Done."
|
53
|
+
puts
|
54
|
+
|
55
|
+
puts <<-END_START_INSTRUCTIONS.trim
|
56
|
+
This agent is reset. You can prepare it for use again
|
57
|
+
at anytime with:
|
58
|
+
|
59
|
+
sudo #{$PROGRAM_NAME} identify
|
60
|
+
|
61
|
+
END_START_INSTRUCTIONS
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def abort_with_agent_running
|
67
|
+
abort <<-END_AGENT_RUNNING.trim
|
68
|
+
You cannot reset while the agent is running. Please stop
|
69
|
+
it first with the following command:
|
70
|
+
|
71
|
+
sudo #{$PROGRAM_NAME} stop
|
72
|
+
|
73
|
+
END_AGENT_RUNNING
|
74
|
+
end
|
75
|
+
|
76
|
+
def abort_with_cancel
|
77
|
+
abort "Reset cancelled. No data was removed."
|
78
|
+
end
|
79
|
+
|
80
|
+
def abort_with_insufficient_permissions(path)
|
81
|
+
abort <<-END_PRIVILEGES.trim
|
82
|
+
I don't have enough privileges to remove '#{path}'.
|
83
|
+
Try running this program again with super user privileges:
|
84
|
+
|
85
|
+
sudo #{$PROGRAM_NAME} reset
|
86
|
+
|
87
|
+
END_PRIVILEGES
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
module ScoutAgent
|
4
|
+
class Assignment
|
5
|
+
class Snapshot < Assignment
|
6
|
+
def execute
|
7
|
+
log = ScoutAgent.prepare_wire_tap(:snapshot, :skip_stdout)
|
8
|
+
|
9
|
+
unless db = Database.load(:snapshots, log)
|
10
|
+
abort_with_missing_db
|
11
|
+
end
|
12
|
+
|
13
|
+
open(__FILE__) do |this_file|
|
14
|
+
unless this_file.flock(File::LOCK_EX | File::LOCK_NB)
|
15
|
+
exit # snapshot in progress
|
16
|
+
end
|
17
|
+
|
18
|
+
log.info("Building snapshot.")
|
19
|
+
status_database(log)
|
20
|
+
status("Building snapshot", :snapshot)
|
21
|
+
at_my_exit do
|
22
|
+
clear_status(:snapshot)
|
23
|
+
end
|
24
|
+
|
25
|
+
commands = db.current_commands
|
26
|
+
|
27
|
+
if commands.empty?
|
28
|
+
if db.have_commands?
|
29
|
+
abort_with_too_recent
|
30
|
+
else
|
31
|
+
log.warn("No commands were found.")
|
32
|
+
abort_with_no_commands
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
snapshot_started = Time.now
|
37
|
+
commands.each do |command|
|
38
|
+
log.info("Running `#{command[:code]}`.")
|
39
|
+
command_started = Time.now
|
40
|
+
output = nil
|
41
|
+
begin
|
42
|
+
Timeout.timeout(command[:timeout]) do
|
43
|
+
output = `sh -c #{shell_escape command[:code]} 2>&1`
|
44
|
+
end
|
45
|
+
rescue Timeout::Error
|
46
|
+
log.error("`#{command[:code]}` took too long to run.")
|
47
|
+
output = "Error: This command took too long to run"
|
48
|
+
end
|
49
|
+
exit_status = $?.exitstatus
|
50
|
+
run_time = Time.now - command_started
|
51
|
+
db.complete_run( command,
|
52
|
+
output,
|
53
|
+
exit_status,
|
54
|
+
snapshot_started,
|
55
|
+
run_time )
|
56
|
+
log.debug( "`#{command[:code]}` exited (#{exit_status}) in " +
|
57
|
+
"#{run_time} seconds." )
|
58
|
+
end
|
59
|
+
|
60
|
+
db.maintain
|
61
|
+
|
62
|
+
log.info("Snapshot complete.")
|
63
|
+
this_file.flock(File::LOCK_UN)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Escape +str+ to make it useable in a shell as one "word".
|
70
|
+
def shell_escape(str)
|
71
|
+
str.to_s.gsub( /(?=[^a-zA-Z0-9_.\/\-\x7F-\xFF\n])/, '\\' ).
|
72
|
+
gsub( /\n/, "'\n'" ).
|
73
|
+
sub( /^$/, "''" )
|
74
|
+
end
|
75
|
+
|
76
|
+
def abort_with_missing_db
|
77
|
+
warn "Snapshots database could not be loaded."
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
|
81
|
+
def abort_with_too_recent
|
82
|
+
warn "A snapshot was recently taken."
|
83
|
+
exit 2
|
84
|
+
end
|
85
|
+
|
86
|
+
def abort_with_no_commands
|
87
|
+
warn "No snapshot commands have been received from the server."
|
88
|
+
exit 3
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
module ScoutAgent
|
4
|
+
class Assignment
|
5
|
+
class Start < Assignment
|
6
|
+
choose_user true
|
7
|
+
choose_group true
|
8
|
+
|
9
|
+
def execute
|
10
|
+
unless Plan.pid_dir.exist?
|
11
|
+
unless Plan.build_pid_dir(group.gid)
|
12
|
+
abort_with_missing_pid_dir
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
unless switch_group_and_user
|
17
|
+
abort_with_wrong_group_or_user
|
18
|
+
end
|
19
|
+
|
20
|
+
unless test_server_connection(:quiet)
|
21
|
+
abort_with_cannot_connect
|
22
|
+
end
|
23
|
+
|
24
|
+
unless Plan.valid? and
|
25
|
+
Plan.pid_dir.exist? and
|
26
|
+
Plan.pid_dir.readable? and
|
27
|
+
Plan.pid_dir.writable?
|
28
|
+
abort_with_missing_resources
|
29
|
+
end
|
30
|
+
|
31
|
+
running_mode = Plan.run_as_daemon? ? " as a daemon" : ""
|
32
|
+
puts "Starting #{ScoutAgent.proper_agent_name}#{running_mode}..."
|
33
|
+
card = IDCard.new(:lifeline)
|
34
|
+
unless card.authorize { not Plan.run_as_daemon? or daemonize }
|
35
|
+
other_process = card.pid
|
36
|
+
if other_process and other_process != Process.pid
|
37
|
+
abort_with_other_process_running(other_process)
|
38
|
+
else
|
39
|
+
abort_with_failure_to_daemonize
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
log = ScoutAgent.prepare_wire_tap(:lifeline)
|
44
|
+
log.info("Loading monitors.")
|
45
|
+
|
46
|
+
lifelines = %w[master communication].map { |agent|
|
47
|
+
Lifeline.new(agent, log)
|
48
|
+
}
|
49
|
+
%w[TERM INT].each do |signal|
|
50
|
+
trap(signal) do
|
51
|
+
lifelines.each { |line| line.terminate }
|
52
|
+
Process.waitall
|
53
|
+
end
|
54
|
+
end
|
55
|
+
lifelines.each { |line| line.launch_and_monitor }
|
56
|
+
|
57
|
+
lifelines.each { |line| line.join }
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def daemonize
|
63
|
+
exit!(0) if fork
|
64
|
+
Process.setsid
|
65
|
+
exit!(0) if fork
|
66
|
+
Dir.chdir("/")
|
67
|
+
File.umask(0000)
|
68
|
+
$stdin.reopen("/dev/null")
|
69
|
+
$stdout.reopen("/dev/null", "w")
|
70
|
+
$stderr.reopen("/dev/null", "w")
|
71
|
+
true
|
72
|
+
rescue Exception # if anything goes wrong
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def switch_group_and_user
|
77
|
+
if Process.euid != user.uid or Process.egid != group.egid
|
78
|
+
Process.initgroups(user.name, group.gid) # prepare groups
|
79
|
+
Process::GID.change_privilege(group.gid) # switch group
|
80
|
+
Process::UID.change_privilege(user.uid) # switch user
|
81
|
+
end
|
82
|
+
true
|
83
|
+
rescue Errno::EPERM # we don't have permission to make the change
|
84
|
+
false
|
85
|
+
end
|
86
|
+
|
87
|
+
def abort_with_missing_pid_dir
|
88
|
+
abort <<-END_PID_DIR.trim
|
89
|
+
Unable to prepare PID file storage. Please start the daemon
|
90
|
+
with super user privileges, which it will relenquish after
|
91
|
+
setup:
|
92
|
+
|
93
|
+
sudo #{$PROGRAM_NAME} start
|
94
|
+
|
95
|
+
END_PID_DIR
|
96
|
+
end
|
97
|
+
|
98
|
+
def abort_with_cannot_connect
|
99
|
+
abort <<-END_CANNOT_CONNECT.trim
|
100
|
+
Unable to load a plan from the server at:
|
101
|
+
|
102
|
+
#{Plan.server_url}
|
103
|
+
|
104
|
+
If that URL looks correct and you are not having network
|
105
|
+
connectivity issues, please verify that the agent_key
|
106
|
+
setting is correct in:
|
107
|
+
|
108
|
+
#{Plan.config_file}
|
109
|
+
|
110
|
+
END_CANNOT_CONNECT
|
111
|
+
end
|
112
|
+
|
113
|
+
def abort_with_wrong_group_or_user
|
114
|
+
abort <<-END_GROUP_USER.trim
|
115
|
+
Unable to switch to the selected user and group. Please
|
116
|
+
start the daemon with super user privileges, which it will
|
117
|
+
relenquish after setup:
|
118
|
+
|
119
|
+
sudo #{$PROGRAM_NAME} start
|
120
|
+
|
121
|
+
END_GROUP_USER
|
122
|
+
end
|
123
|
+
|
124
|
+
def abort_with_missing_resources
|
125
|
+
abort <<-END_RESOURCES.trim
|
126
|
+
Some resources needed to complete the startup process are
|
127
|
+
not available. Please make sure you have setup the daemon
|
128
|
+
with this command:
|
129
|
+
|
130
|
+
sudo #{$PROGRAM_NAME} identify
|
131
|
+
|
132
|
+
and that you have started the daemon with super user
|
133
|
+
privileges using this command:
|
134
|
+
|
135
|
+
sudo #{$PROGRAM_NAME} start
|
136
|
+
|
137
|
+
END_RESOURCES
|
138
|
+
end
|
139
|
+
|
140
|
+
def abort_with_other_process_running(pid)
|
141
|
+
abort "The daemon is already running with the process ID of #{pid}."
|
142
|
+
end
|
143
|
+
|
144
|
+
def abort_with_failure_to_daemonize
|
145
|
+
abort "Unable to daemonize this process."
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
module ScoutAgent
|
4
|
+
class Assignment
|
5
|
+
class Status < Assignment
|
6
|
+
def execute
|
7
|
+
unless db = status_database
|
8
|
+
abort_with_missing_db
|
9
|
+
end
|
10
|
+
|
11
|
+
puts "Status"
|
12
|
+
puts "======"
|
13
|
+
puts
|
14
|
+
statuses = db.current_statuses
|
15
|
+
if statuses.empty?
|
16
|
+
puts "#{ScoutAgent.proper_agent_name} is not currently running."
|
17
|
+
else
|
18
|
+
puts "#{ScoutAgent.proper_agent_name} is running:"
|
19
|
+
puts
|
20
|
+
columns = [
|
21
|
+
%w[Process name],
|
22
|
+
%w[PID pid],
|
23
|
+
%w[Current\ Task status],
|
24
|
+
%w[Last\ Updated last_updated_at]
|
25
|
+
].map { |title, data| [title] + statuses.map { |row| row[data] } }
|
26
|
+
sizes = columns.map { |column|
|
27
|
+
column.map { |field| field.to_s.size }.max }
|
28
|
+
format = sizes.map { |size| "%-#{size}s" }.join(" ")
|
29
|
+
puts format % columns.map { |column| column.first }
|
30
|
+
puts format % sizes.map { |size| "-" * size }
|
31
|
+
columns.first[1..-1].
|
32
|
+
zip(*columns[1..-1].map { |c| c[1..-1] }) do |row|
|
33
|
+
puts format % row
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def abort_with_missing_db
|
39
|
+
warn "Statuses database could not be loaded."
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|