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