scout_agent 3.0.5 → 3.0.6

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 CHANGED
@@ -1,3 +1,21 @@
1
+ == 3.0.6
2
+
3
+ * The new `sudo scout_agent update` command can be used to update your
4
+ configuration file
5
+ * Made it possible to disable XMPP for a smaller memory footprint
6
+ * Restricted XMPP commands to a white list for added security
7
+ * Added an optional "force" argument for `scout_agent snapshot` that clears
8
+ command run times before running to ensure a full snapshot
9
+ * Modified XMPP snapshots to use the new force option for snapshots to ensure
10
+ requests from users via the Web interface are always honored
11
+ * Added a periodic_snapshots configuration option that defaults to true and will
12
+ cause the agent to attempt a snapshot before each check-in (though command
13
+ intervals will usually only allow them to be run every few check-ins)
14
+ * Fixed a bug that could cause snapshots to temporarily orphan a process and/or
15
+ to unnecessarily delay API requests
16
+ * Fixed a bug that could cause the agent to crash if it had trouble killing off
17
+ a spawned process
18
+
1
19
  == 3.0.5
2
20
 
3
21
  * Added support for XMPP message SHA, instead of just an ID
@@ -2,7 +2,6 @@
2
2
  # encoding: UTF-8
3
3
 
4
4
  # load agent extensions
5
- require "scout_agent/api"
6
5
  require "scout_agent/order"
7
6
 
8
7
  module ScoutAgent
@@ -20,6 +19,10 @@ module ScoutAgent
20
19
  @jabber = nil
21
20
  @roster = nil
22
21
  @shutdown_thread = nil
22
+ @trusted = Array(Plan.xmpp_trusted).map { |trusted|
23
+ trusted_with_server = trusted.sub(/@\*\z/, "@#{jabber_server}")
24
+ /\A#{Regexp.escape(trusted_with_server)}\b/
25
+ }
23
26
  end
24
27
 
25
28
  def run
@@ -104,8 +107,13 @@ module ScoutAgent
104
107
 
105
108
  def install_subscriptions_callback
106
109
  @roster.add_subscription_request_callback do |_, presence|
107
- log.info("Accepting subscription: #{presence.from}")
108
- @roster.accept_subscription(presence.from)
110
+ log.info("Subscription request: #{presence.from}")
111
+ if trusted? presence.from
112
+ log.info("Request accepted.")
113
+ @roster.accept_subscription(presence.from)
114
+ else
115
+ log.warn("Untrusted source, ignoring.")
116
+ end
109
117
  end
110
118
  end
111
119
 
@@ -113,16 +121,20 @@ module ScoutAgent
113
121
  @jabber.add_message_callback do |message|
114
122
  log.info("Received message from #{message.from}: #{message.body}")
115
123
  status("Parsing command")
116
- modified_body = nil
117
- if message.body =~ /\A\s*\[\s*(\S+)\s*\]\s*(.*)/
118
- modified_body = $2
119
- send_chat_message(message.from, "received #{$1}")
120
- end
121
- if order = Order.can_handle?(message, modified_body)
122
- status("Processing command")
123
- order.execute
124
+ if trusted? message.from
125
+ modified_body = nil
126
+ if message.body =~ /\A\s*\[\s*(\S+)\s*\]\s*(.*)/
127
+ modified_body = $2
128
+ send_chat_message(message.from, "received #{$1}")
129
+ end
130
+ if order = Order.can_handle?(message, modified_body)
131
+ status("Processing command")
132
+ order.execute
133
+ else
134
+ log.warn("Unrecognized message format, ignoring.")
135
+ end
124
136
  else
125
- log.warn("Unrecognized message format, ingoring.")
137
+ log.warn("Untrusted source, ignoring.")
126
138
  end
127
139
  status("Listening for commands")
128
140
  end
@@ -168,6 +180,11 @@ module ScoutAgent
168
180
  @jabber_server ||= Plan.test_mode? ? "jabber.org" :
169
181
  URI.parse(Plan.server_url).host
170
182
  end
183
+
184
+ def trusted?(user)
185
+ id = user.to_s
186
+ @trusted.any? { |trusted| id =~ trusted }
187
+ end
171
188
  end
172
189
  end
173
190
  end
@@ -28,10 +28,11 @@ module ScoutAgent
28
28
  @main_loop = Thread.new do
29
29
  Thread.current.abort_on_exception = true
30
30
  loop do
31
- %w[ fetch_plan
32
- execute_missions
31
+ %w[ fetch_plan
32
+ execute_missions
33
+ prepare_snapshot
33
34
  checkin
34
- perform_maintenance
35
+ perform_maintenance
35
36
  wait_for_orders ].each do |stage|
36
37
  send(stage)
37
38
  check_running_status
@@ -123,12 +124,12 @@ module ScoutAgent
123
124
  rescue Timeout::Error # mission exceeded allowed execution
124
125
  status = Process.term_or_kill(pid)
125
126
  log.error( "#{mission[:name]} took too long to run: " +
126
- "#{status.exitstatus}." )
127
+ "#{status && status.exitstatus}." )
127
128
  @db.write_report(
128
129
  mission[:id],
129
130
  :error,
130
131
  :subject => "#{mission[:name]} took too long to run",
131
- :body => "Exit status: #{status.exitstatus}"
132
+ :body => "Exit status: #{status && status.exitstatus}"
132
133
  )
133
134
  end
134
135
  # prevent an infinite loop if we can't complete the mission
@@ -140,6 +141,15 @@ module ScoutAgent
140
141
  log.warn("No missions to run.") unless ran_a_mission
141
142
  end
142
143
 
144
+ def prepare_snapshot
145
+ if Plan.periodic_snapshots?
146
+ status("Preparing a system snapshot")
147
+ log.info( "Requesting a system snapshot, if command intervals have " +
148
+ "been exceeded." )
149
+ API.take_snapshot
150
+ end
151
+ end
152
+
143
153
  def checkin
144
154
  reports = @db.current_reports
145
155
  queued = @queue.queued_reports
@@ -242,6 +242,10 @@ module ScoutAgent
242
242
  QueueCommand.new(:error, fields, options)
243
243
  end
244
244
 
245
+ #
246
+ # :call-seq:
247
+ # take_snapshot(options = { })
248
+ # take_snapshot(force = false, options = { })
245
249
  #
246
250
  # This method requests that the agent take a snapshot of the current
247
251
  # environment, by running any commands the server has sent down. A
@@ -249,8 +253,16 @@ module ScoutAgent
249
253
  # during the next checkin. The returned object and background processing
250
254
  # rules are the same as those described in queue_for_mission().
251
255
  #
252
- def take_snapshot(options = { })
253
- Command.new(:snapshot, nil, nil, options)
256
+ # Passing a `true` value in `force` clears the command times before running,
257
+ # forcing a full snapshot to be taken.
258
+ #
259
+ def take_snapshot(*args)
260
+ started = Time.now
261
+ force = args.shift ? %w[force] : nil unless args.first.is_a? Hash
262
+ options = args.shift || { }
263
+ Command.new(:snapshot, force, nil, options)
264
+ ensure
265
+ p Time.now - started
254
266
  end
255
267
  end
256
268
  end
@@ -22,6 +22,11 @@ module ScoutAgent
22
22
  at_my_exit do
23
23
  clear_status(:snapshot)
24
24
  end
25
+
26
+ if Array(other_args).shift == "force"
27
+ log.info("Clearing command run times to force a full snapshot.")
28
+ db.reset_all_commands
29
+ end
25
30
 
26
31
  commands = db.current_commands
27
32
 
@@ -38,16 +43,30 @@ module ScoutAgent
38
43
  commands.each do |command|
39
44
  log.info("Running `#{command[:code]}`.")
40
45
  command_started = Time.now
46
+ reader, writer = IO.pipe
47
+ run = fork do
48
+ reader.close
49
+ STDOUT.reopen(writer)
50
+ STDERR.reopen(writer)
51
+ begin
52
+ exec(command[:code])
53
+ rescue Exception # failed to execute
54
+ warn "#{$!.message} (#{$!.class})"
55
+ end
56
+ end
57
+ exit_status = nil
41
58
  output = nil
59
+ writer.close
42
60
  begin
43
61
  Timeout.timeout(command[:timeout]) do
44
- output = `sh -c #{shell_escape command[:code]} 2>&1`
62
+ output = reader.read
63
+ exit_status = Process.wait2(run).last
45
64
  end
46
65
  rescue Timeout::Error
47
66
  log.error("`#{command[:code]}` took too long to run.")
67
+ exit_status = Process.term_or_kill(run)
48
68
  output = "Error: This command took too long to run"
49
69
  end
50
- exit_status = $?.exitstatus
51
70
  run_time = Time.now - command_started
52
71
  db.complete_run( command,
53
72
  output,
@@ -67,13 +86,6 @@ module ScoutAgent
67
86
 
68
87
  private
69
88
 
70
- # Escape +str+ to make it useable in a shell as one "word".
71
- def shell_escape(str)
72
- str.to_s.gsub( /(?=[^a-zA-Z0-9_.\/\-\x7F-\xFF\n])/n, '\\' ).
73
- gsub( /\n/, "'\n'" ).
74
- sub( /^$/, "''" )
75
- end
76
-
77
89
  def abort_with_missing_db
78
90
  warn "Snapshots database could not be loaded."
79
91
  exit 1
@@ -44,9 +44,10 @@ module ScoutAgent
44
44
  log = ScoutAgent.prepare_wire_tap(:lifeline)
45
45
  log.info("Loading monitors.")
46
46
 
47
- lifelines = %w[master communication].map { |agent|
48
- Lifeline.new(agent, log)
49
- }
47
+ agents = %w[master]
48
+ agents << "communication" if Plan.enable_xmpp?
49
+
50
+ lifelines = agents.map { |agent| Lifeline.new(agent, log) }
50
51
  %w[TERM INT].each do |signal|
51
52
  trap(signal) do
52
53
  Thread.new do
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby -wKU
2
+ # encoding: UTF-8
3
+
4
+ module ScoutAgent
5
+ class Assignment
6
+ class Update < Assignment
7
+ def execute
8
+ begin
9
+ Plan.config_file.unlink
10
+ puts "Identification file '#{Plan.config_file}' removed."
11
+ rescue Errno::EACCES
12
+ abort_with_sudo_required
13
+ end
14
+ if Plan.write_default_config_file
15
+ puts "Identification file '#{Plan.config_file}' replaced."
16
+ else
17
+ if Plan.config_file.exist?
18
+ puts "Identification file '#{Plan.config_file}' exists. Skipped."
19
+ else
20
+ abort_with_sudo_required
21
+ end
22
+ end
23
+ end
24
+
25
+ def abort_with_sudo_required
26
+ abort <<-END_SUDO_REQUIRED.trim
27
+ I don't have enough privileges to update your identity and
28
+ configuration. Try running this program again with super
29
+ user privileges:
30
+
31
+ sudo #{$PROGRAM_NAME} identify
32
+
33
+ END_SUDO_REQUIRED
34
+ end
35
+ end
36
+ end
37
+ end
@@ -51,6 +51,10 @@ module ScoutAgent
51
51
  end
52
52
  end
53
53
 
54
+ ################
55
+ ### Commands ###
56
+ ################
57
+
54
58
  #
55
59
  # Updates the list of snapshot +commands+ in the database. Existing
56
60
  # commands are updated, new commands are added, and commands no longer in
@@ -131,6 +135,26 @@ module ScoutAgent
131
135
  nil # commands not found
132
136
  end
133
137
 
138
+ #
139
+ # All commands are reset so they will be run again at the first available
140
+ # opportunity.
141
+ #
142
+ def reset_all_commands
143
+ write_to_sqlite do |sqlite|
144
+ sqlite.execute(<<-END_RESET_COMMANDS.trim)
145
+ UPDATE commands
146
+ SET next_run_at = strftime('%Y-%m-%d %H:%M', 'now', 'localtime')
147
+ END_RESET_COMMANDS
148
+ end
149
+ rescue Amalgalite::SQLite3::Error => error # failed to reset commands
150
+ # do nothing: commands will be run at their scheduled time
151
+ log.error("Database command reset error: #{error.message}.")
152
+ end
153
+
154
+ ############
155
+ ### Runs ###
156
+ ############
157
+
134
158
  #
135
159
  # Marks +command+ as just having run in the database and updates its
136
160
  # +next_run_at+ Time. A run is also created for the +command+ documenting
@@ -44,6 +44,10 @@ module ScoutAgent
44
44
  "The minimum level of log message to record." ) do |level|
45
45
  switches[:logging_level] = level
46
46
  end
47
+ opts.on( "-x", "--[no-]xmpp",
48
+ "Allow the server to send XMPP commands." ) do |boolean|
49
+ switches[:enable_xmpp] = boolean
50
+ end
47
51
  opts.on( "-t", "--[no-]test-mode",
48
52
  "Used in agent development." ) do |boolean|
49
53
  if switches[:test_mode] = boolean
@@ -53,6 +57,14 @@ module ScoutAgent
53
57
  end
54
58
 
55
59
  opts.separator "Expert Options:"
60
+ opts.on( "--[no-]periodic-snapshots",
61
+ "Take regular system snapshots." ) do |boolean|
62
+ switches[:periodic_snapshots] = boolean
63
+ end
64
+ opts.on( "--trusted NAME1,NAME2,...", Array,
65
+ "A list of trusted XMPP users." ) do |users|
66
+ switches[:xmpp_trusted] = users
67
+ end
56
68
  opts.on( "--users NAME1,NAME2,...", Array,
57
69
  "A list of users to try switching to." ) do |users|
58
70
  switches[:user_choices] = users
@@ -145,9 +157,9 @@ module ScoutAgent
145
157
  end
146
158
 
147
159
  def abort_with_ambiguous_assignment(assignment, matches)
148
- choices = matches.map { |m| "'#{m.basename('.rb')}'" }
160
+ choices = matches.map { |m| "'#{File.basename(m, '.rb')}'" }
149
161
  choices[-2..-1] = choices[-2..-1].join(", or ")
150
- abort <<-END_AMBIGUOUS
162
+ abort <<-END_AMBIGUOUS.trim
151
163
  Ambiguous command '#{assignment}'. Did you mean #{choices.join(', ')}?
152
164
  END_AMBIGUOUS
153
165
  end
@@ -8,7 +8,7 @@ module ScoutAgent
8
8
 
9
9
  def execute
10
10
  log.info("Requesting that a snapshot be taken.")
11
- API.take_snapshot
11
+ API.take_snapshot(:force)
12
12
  notify_master
13
13
  end
14
14
  end
@@ -17,23 +17,26 @@ module ScoutAgent
17
17
 
18
18
  # The default configuration for this agent.
19
19
  def defaults
20
- [ [:server_url, "http://beta.scoutapp.com:3000"],
21
- [:proxy_url, nil],
22
- [:run_as_daemon, true],
23
- [:logging_level, "INFO"],
24
- [:test_mode, false],
20
+ [ [:server_url, "http://beta.scoutapp.com:3000"],
21
+ [:proxy_url, nil],
22
+ [:run_as_daemon, true],
23
+ [:logging_level, "INFO"],
24
+ [:enable_xmpp, true],
25
+ [:test_mode, false],
25
26
 
26
- [:user_choices, %w[daemon nobody]],
27
- [:group_choices, %w[daemon nogroup]],
28
- [:prefix_path, "/"],
29
- [:os_config_path, "etc"],
30
- [:os_db_path, "var/db"],
31
- [:os_pid_path, "var/run"],
32
- [:os_log_path, "var/log"],
33
- [:config_file, nil],
34
- [:db_dir, nil],
35
- [:pid_dir, nil],
36
- [:log_dir, nil] ]
27
+ [:periodic_snapshots, true],
28
+ [:xmpp_trusted, %w[scout@*]],
29
+ [:user_choices, %w[daemon nobody]],
30
+ [:group_choices, %w[daemon nogroup]],
31
+ [:prefix_path, "/"],
32
+ [:os_config_path, "etc"],
33
+ [:os_db_path, "var/db"],
34
+ [:os_pid_path, "var/run"],
35
+ [:os_log_path, "var/log"],
36
+ [:config_file, nil],
37
+ [:db_dir, nil],
38
+ [:pid_dir, nil],
39
+ [:log_dir, nil] ]
37
40
  end
38
41
 
39
42
  # This method is used to set or reset the default configuration.
@@ -45,15 +48,25 @@ module ScoutAgent
45
48
  alias_method :reset_defaults, :set_defaults
46
49
 
47
50
  # Provide a more natural interface for a boolean flag.
48
- def run_as_daemon? # can't use alias_method() because it's not defined yet
51
+ def run_as_daemon? # can't use alias_method() because it's not defined
49
52
  run_as_daemon
50
53
  end
51
54
 
52
55
  # Provide a more natural interface for a boolean flag.
53
- def test_mode? # can't use alias_method() because it's not defined yet
56
+ def test_mode? # can't use alias_method() because it's not defined
54
57
  test_mode
55
58
  end
56
59
 
60
+ # Provide a more natural interface for a boolean flag.
61
+ def enable_xmpp? # can't use alias_method() because it's not defined
62
+ enable_xmpp
63
+ end
64
+
65
+ # Provide a more natural interface for a boolean flag.
66
+ def periodic_snapshots? # can't use alias_method() because it's not defined
67
+ periodic_snapshots
68
+ end
69
+
57
70
  ###########################
58
71
  ### Configuration Files ###
59
72
  ###########################
@@ -74,10 +87,12 @@ module ScoutAgent
74
87
  #
75
88
  def write_default_config_file
76
89
  config_file.dirname.mkpath
77
- config_file.open(File::CREAT|File::EXCL|File::WRONLY) do |pid|
90
+ config_file.open(File::CREAT | File::EXCL | File::WRONLY) do |pid|
78
91
  pid.puts <<-END_DEFAULT_CONFIG.trim
79
92
  #!/usr/bin/env ruby -wKU
80
93
  # encoding: UTF-8
94
+
95
+ # #{ScoutAgent.proper_agent_name} v#{ScoutAgent::VERSION}
81
96
 
82
97
  #
83
98
  # This file configures #{ScoutAgent.proper_agent_name}. The settings in
@@ -133,10 +148,60 @@ module ScoutAgent
133
148
  #
134
149
  config.logging_level = #{logging_level.inspect}
135
150
 
151
+ #
152
+ # The XMPP features of the agent are used to allow the server to remain
153
+ # in contact. The server can use this to send some simple commands to
154
+ # the agent as you interact with the Web interface. The server may also
155
+ # use this to monitor when the agent is online.
156
+ #
157
+ # This feature takes some extra memory due to maintaining the XMPP
158
+ # connection. Set the following to false if you don't need this
159
+ # interactivity and you would prefer to see the agent run with a smaller
160
+ # footprint.
161
+ #
162
+ config.enable_xmpp = #{enable_xmpp.inspect}
163
+
136
164
  #####################
137
165
  ### Expert Config ###
138
166
  #####################
139
167
 
168
+ #
169
+ # The following setting causes the agent to attempt taking regular
170
+ # snapshots of the system. An attempt is made before each check-in, but
171
+ # command intervals will usually only allow them to be run after a few
172
+ # check-ins.
173
+ #
174
+ # Change this setting to false if you would prefer to manage when
175
+ # snapshots are taken manually using the shell command or the API.
176
+ #
177
+ config.periodic_snapshots = #{periodic_snapshots.inspect}
178
+
179
+ #
180
+ # The following list of Jabber users are trusted to send the agent
181
+ # commands. Only commands from from a user matching one of the names
182
+ # in this list will be executed.
183
+ #
184
+ # The names in this list must match at the beginning of the user's name
185
+ # and stop at a word boundary. Thus, all of the following would match
186
+ # the user "scout@scoutapp.com":
187
+ #
188
+ # scout
189
+ # scout@scoutapp
190
+ # scout@scoutapp.com
191
+ #
192
+ # Note that the first name would match a "scout" user from any domain
193
+ # and the second would match a ".com", ".net", ".org", or whatever, but
194
+ # the third requires a full match. However, "scout@scout" will not
195
+ # match "scout@scoutapp.com" since it doesn't end at a boundary.
196
+ #
197
+ # There's also a special case where any name ending in "@*" is modified
198
+ # to have the "*" replaced with the host name from config.server_url.
199
+ #
200
+ # Feel free to add Jabber (XMPP) users to the list so they too can
201
+ # monitor and command the agent from their IM client.
202
+ #
203
+ config.xmpp_trusted = %w[#{xmpp_trusted.join(" ")}]
204
+
140
205
  #
141
206
  # The agent will try to use standard Unix locations to store its
142
207
  # data, but you are free to adjust these paths as needed.
data/lib/scout_agent.rb CHANGED
@@ -24,6 +24,7 @@ require "scout_agent/id_card"
24
24
  require "scout_agent/assignment"
25
25
  require "scout_agent/lifeline"
26
26
  require "scout_agent/dispatcher"
27
+ require "scout_agent/api"
27
28
 
28
29
  # require gems
29
30
  require_lib_or_gem "json", "=1.1.4"
@@ -65,7 +66,7 @@ module ScoutAgent
65
66
  end
66
67
 
67
68
  # The version of this agent.
68
- VERSION = "3.0.5".freeze
69
+ VERSION = "3.0.6".freeze
69
70
  # A Pathname reference to the agent code directory, used in dynamic loading.
70
71
  LIB_DIR = Pathname.new(File.dirname(__FILE__)) + agent_name
71
72
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.5
4
+ version: 3.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Edward Gray II
@@ -12,7 +12,7 @@ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
14
 
15
- date: 2009-04-14 00:00:00 -05:00
15
+ date: 2009-04-17 00:00:00 -05:00
16
16
  default_executable:
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
@@ -102,6 +102,7 @@ files:
102
102
  - lib/scout_agent/assignment/start.rb
103
103
  - lib/scout_agent/assignment/status.rb
104
104
  - lib/scout_agent/assignment/stop.rb
105
+ - lib/scout_agent/assignment/update.rb
105
106
  - lib/scout_agent/assignment/upload_log.rb
106
107
  - lib/scout_agent/assignment.rb
107
108
  - lib/scout_agent/core_extensions.rb