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