scout_agent 3.0.5 → 3.0.6

Sign up to get free protection for your applications and to get access to all the features.
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