scout_agent 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/AUTHORS +4 -0
  2. data/CHANGELOG +3 -0
  3. data/COPYING +340 -0
  4. data/INSTALL +17 -0
  5. data/LICENSE +6 -0
  6. data/README +3 -0
  7. data/Rakefile +123 -0
  8. data/TODO +3 -0
  9. data/bin/scout_agent +11 -0
  10. data/lib/scout_agent.rb +73 -0
  11. data/lib/scout_agent/agent.rb +42 -0
  12. data/lib/scout_agent/agent/communication_agent.rb +85 -0
  13. data/lib/scout_agent/agent/master_agent.rb +301 -0
  14. data/lib/scout_agent/api.rb +241 -0
  15. data/lib/scout_agent/assignment.rb +105 -0
  16. data/lib/scout_agent/assignment/configuration.rb +30 -0
  17. data/lib/scout_agent/assignment/identify.rb +110 -0
  18. data/lib/scout_agent/assignment/queue.rb +95 -0
  19. data/lib/scout_agent/assignment/reset.rb +91 -0
  20. data/lib/scout_agent/assignment/snapshot.rb +92 -0
  21. data/lib/scout_agent/assignment/start.rb +149 -0
  22. data/lib/scout_agent/assignment/status.rb +44 -0
  23. data/lib/scout_agent/assignment/stop.rb +60 -0
  24. data/lib/scout_agent/assignment/upload_log.rb +61 -0
  25. data/lib/scout_agent/core_extensions.rb +260 -0
  26. data/lib/scout_agent/database.rb +386 -0
  27. data/lib/scout_agent/database/mission_log.rb +282 -0
  28. data/lib/scout_agent/database/queue.rb +126 -0
  29. data/lib/scout_agent/database/snapshots.rb +187 -0
  30. data/lib/scout_agent/database/statuses.rb +65 -0
  31. data/lib/scout_agent/dispatcher.rb +157 -0
  32. data/lib/scout_agent/id_card.rb +143 -0
  33. data/lib/scout_agent/lifeline.rb +243 -0
  34. data/lib/scout_agent/mission.rb +212 -0
  35. data/lib/scout_agent/order.rb +58 -0
  36. data/lib/scout_agent/order/check_in_order.rb +32 -0
  37. data/lib/scout_agent/order/snapshot_order.rb +33 -0
  38. data/lib/scout_agent/plan.rb +306 -0
  39. data/lib/scout_agent/server.rb +123 -0
  40. data/lib/scout_agent/tracked.rb +59 -0
  41. data/lib/scout_agent/wire_tap.rb +513 -0
  42. data/setup.rb +1360 -0
  43. data/test/tc_core_extensions.rb +89 -0
  44. data/test/tc_id_card.rb +115 -0
  45. data/test/tc_plan.rb +285 -0
  46. data/test/test_helper.rb +22 -0
  47. data/test/ts_all.rb +7 -0
  48. 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