scout 5.3.2 → 5.3.3

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,9 @@
1
+ == 5.3.3
2
+
3
+ * Sending embedded options to server for local & plugin overrides.
4
+ * Reading options for local plugins
5
+ * Added support for an account-specific public key (scout_rsa.pub)
6
+
1
7
  == 5.3.2
2
8
 
3
9
  * New --name="My Server" option to specify server name from the scout command.
data/lib/scout.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby -wKU
2
2
 
3
3
  module Scout
4
- VERSION = "5.3.2".freeze
4
+ VERSION = "5.3.3".freeze
5
5
  end
6
6
 
7
7
  require "scout/command"
data/lib/scout/command.rb CHANGED
@@ -44,6 +44,8 @@ module Scout
44
44
  "PATH_TO_PLUGIN [PLUGIN_OPTIONS]"
45
45
  opts.separator "[PLUGIN_OPTIONS] format: opt1=val1 opt2=val2 opt2=val3 ..."
46
46
  opts.separator "Plugin will use internal defaults if options aren't provided."
47
+ opts.separator " Sign Code:"
48
+ opts.separator " #{program_name} [OPTIONS] sign PATH_TO_PLUGIN"
47
49
  opts.separator " "
48
50
  opts.separator "Note: This client is meant to be installed and"
49
51
  opts.separator "invoked through cron or any other scheduler."
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ require "pp"
4
+ require "openssl"
5
+ module Scout
6
+ class Command
7
+ class Sign < Command
8
+ HELP_URL = "https://scoutapp.com/info/creating_a_plugin#private_plugins"
9
+ CA_FILE = File.join( File.dirname(__FILE__),
10
+ *%w[.. .. .. data cacert.pem] )
11
+ VERIFY_MODE = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
12
+
13
+ def run
14
+ url, *provided_options = @args
15
+ # read the plugin_code from the file specified
16
+ if url.nil? or url == ''
17
+ puts "Please specify the path to the plugin (scout sign /path/to/plugin.rb)"
18
+ return
19
+ end
20
+
21
+ code=fetch_code(url)
22
+ if code.nil?
23
+ return
24
+ end
25
+
26
+ private_key = load_private_key
27
+ if private_key.nil?
28
+ return
29
+ end
30
+
31
+ puts "Signing code..."
32
+ code=code.gsub(/ +$/,'')
33
+ code_signature = private_key.sign( OpenSSL::Digest::SHA1.new, code)
34
+ sig=Base64.encode64(code_signature)
35
+
36
+ puts "Posting Signature..."
37
+ uri = URI.parse(url)
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ if uri.is_a?(URI::HTTPS)
40
+ http.use_ssl = true
41
+ http.ca_file = CA_FILE
42
+ http.verify_mode = VERIFY_MODE
43
+ end
44
+ request = Net::HTTP::Post.new(uri.request_uri)
45
+ request.set_form_data({'signature' => sig})
46
+ res = http.request(request)
47
+ if !res.is_a?(Net::HTTPOK)
48
+ puts "ERROR - Unable to post signature"
49
+ return
50
+ end
51
+ puts "...Success!"
52
+ rescue Timeout::Error
53
+ puts "ERROR - Unable to sign code (Timeout)"
54
+ rescue
55
+ puts "ERROR - Unable to sign code:"
56
+ puts $!
57
+ puts $!.backtrace
58
+ end # run
59
+
60
+ def fetch_code(url)
61
+ puts "Fetching code..."
62
+ uri = URI.parse(url)
63
+ http = Net::HTTP.new(uri.host, uri.port)
64
+ if uri.is_a?(URI::HTTPS)
65
+ http.use_ssl = true
66
+ http.ca_file = CA_FILE
67
+ http.verify_mode = VERIFY_MODE
68
+ end
69
+ request = Net::HTTP::Get.new(uri.request_uri)
70
+ res = http.request(request)
71
+ if !res.is_a?(Net::HTTPOK)
72
+ puts "ERROR - Unable to fetch code: #{res.class}."
73
+ return
74
+ end
75
+ res.body
76
+ end
77
+
78
+ def load_private_key
79
+ private_key_path=File.expand_path("~/.scout/scout_rsa")
80
+ if !File.exist?(private_key_path)
81
+ puts "ERROR - Unable to find the private key at #{private_key_path} for code signing.\nSee #{HELP_URL} for help creating your account's key pair."
82
+ return nil
83
+ else
84
+ begin
85
+ OpenSSL::PKey::RSA.new(File.read(private_key_path))
86
+ rescue
87
+ puts "Error - Found a private key at #{private_key_path}, but unable to load the key:"
88
+ puts $!.message
89
+ puts "See #{HELP_URL} for help creating your account's key pair."
90
+ return nil
91
+ end
92
+ end
93
+ end # load_private_key
94
+ end
95
+ end
96
+ end
@@ -5,7 +5,7 @@ require 'yaml'
5
5
  module Scout
6
6
  # a data structure of an individual plugin option
7
7
  class PluginOption
8
- attr_reader :name, :notes, :default, :advanced, :password, :required
8
+ attr_reader :name, :notes, :default, :advanced, :password, :required, :hash
9
9
  def initialize(name, h)
10
10
  @name=name
11
11
  @notes=h['notes'] || ''
@@ -14,6 +14,7 @@ module Scout
14
14
  @advanced = @attributes.include?('advanced')
15
15
  @password = @attributes.include?('password')
16
16
  @required = @attributes.include?('required')
17
+ @hash = h
17
18
  end
18
19
 
19
20
  # convenience -- for nicer syntax
@@ -27,6 +28,10 @@ module Scout
27
28
  default_string = default == '' ? '' : " Default: #{default}. "
28
29
  "'#{name}'#{required_string}#{default_string}#{notes}"
29
30
  end
31
+
32
+ def to_hash
33
+ @hash
34
+ end
30
35
  end
31
36
 
32
37
  # A collection of pluginOption
@@ -43,7 +48,7 @@ module Scout
43
48
  # attributes: required advanced
44
49
  class PluginOptions < Array
45
50
 
46
- attr_accessor :error
51
+ attr_accessor :error, :hash
47
52
 
48
53
  # Should be valid YAML, a hash of hashes ... if not, will be caught in the rescue below
49
54
  def self.from_yaml(string)
@@ -57,6 +62,7 @@ module Scout
57
62
  ensure
58
63
  res=PluginOptions.new(options_array)
59
64
  res.error=error
65
+ res.hash = items unless res.error
60
66
  return res
61
67
  end
62
68
 
@@ -75,6 +81,10 @@ module Scout
75
81
  end
76
82
  res.join("\n")
77
83
  end
84
+
85
+ def to_hash
86
+ hash
87
+ end
78
88
 
79
89
  end
80
90
  end
data/lib/scout/server.rb CHANGED
@@ -49,10 +49,12 @@ module Scout
49
49
  @logger = logger
50
50
  @server_name = server_name
51
51
  @plugin_plan = []
52
+ @plugins_with_signature_errors = []
52
53
  @directives = {} # take_snapshots, interval, sleep_interval
53
54
  @new_plan = false
54
55
  @local_plugin_path = File.dirname(history_file) # just put overrides and ad-hoc plugins in same directory as history file.
55
56
  @plugin_config_path = File.join(@local_plugin_path, "plugins.properties")
57
+ @account_public_key_path = File.join(@local_plugin_path, "scout_rsa.pub")
56
58
  @plugin_config = load_plugin_configs(@plugin_config_path)
57
59
 
58
60
  # the block is only passed for install and test, since we split plan retrieval outside the lockfile for run
@@ -64,7 +66,7 @@ module Scout
64
66
  end
65
67
 
66
68
  def refresh?
67
- return true if !ping_key
69
+ return true if !ping_key or account_public_key_changed? # fetch the plan again if the account key is modified/created
68
70
 
69
71
  url=URI.join( @server.sub("https://","http://"), "/clients/#{ping_key}/ping.scout")
70
72
 
@@ -103,52 +105,53 @@ module Scout
103
105
  if res["Content-Encoding"] == "gzip" and body and not body.empty?
104
106
  body = Zlib::GzipReader.new(StringIO.new(body)).read
105
107
  end
106
-
108
+
107
109
  body_as_hash = JSON.parse(body)
108
-
109
- # Ensure all the plugins in the new plan are properly signed. Load the public key for this.
110
- public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_id_rsa.pub] ))
111
- debug "Loaded public key used for verifying code signatures (#{public_key_text.size} bytes)"
112
- code_public_key = OpenSSL::PKey::RSA.new(public_key_text)
113
-
110
+
114
111
  temp_plugins=Array(body_as_hash["plugins"])
115
- plugin_signature_error = false
116
- temp_plugins.each do |plugin|
112
+ temp_plugins.each_with_index do |plugin,i|
117
113
  signature=plugin['signature']
118
114
  id_and_name = "#{plugin['id']}-#{plugin['name']}".sub(/\A-/, "")
119
115
  if signature
120
116
  code=plugin['code'].gsub(/ +$/,'') # we strip trailing whitespace before calculating signatures. Same here.
121
117
  decoded_signature=Base64.decode64(signature)
122
- if !code_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code)
123
- info "#{id_and_name} signature doesn't match!"
124
- plugin_signature_error=true
118
+ if !scout_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code)
119
+ if account_public_key
120
+ if !account_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code)
121
+ info "#{id_and_name} signature verification failed for both the Scout and account public keys"
122
+ plugin['sig_error'] = "The code signature failed verification against both the Scout and account public key. Please ensure the public key installed at #{@account_public_key_path} was generated with the same private key used to sign the plugin."
123
+ @plugins_with_signature_errors << temp_plugins.delete_at(i)
124
+ end
125
+ else
126
+ info "#{id_and_name} signature doesn't match!"
127
+ plugin['sig_error'] = "The code signature failed verification. Please place your account-specific public key at #{@account_public_key_path}."
128
+ @plugins_with_signature_errors << temp_plugins.delete_at(i)
129
+ end
125
130
  end
131
+ # filename is set for local plugins. these don't have signatures.
132
+ elsif plugin['filename']
133
+ plugin['code']=nil # should not have any code.
126
134
  else
127
135
  info "#{id_and_name} has no signature!"
128
- plugin_signature_error=true
136
+ plugin['sig_error'] = "The code has no signature and cannot be verified."
137
+ @plugins_with_signature_errors << temp_plugins.delete_at(i)
129
138
  end
130
139
  end
131
140
 
141
+ @plugin_plan = temp_plugins
142
+ @directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new
143
+ @history["plan_last_modified"] = res["last-modified"]
144
+ @history["old_plugins"] = @plugin_plan
145
+ @history["directives"] = @directives
132
146
 
133
- if(!plugin_signature_error)
134
- @plugin_plan = temp_plugins
135
- @directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new
136
- @history["plan_last_modified"] = res["last-modified"]
137
- @history["old_plugins"] = @plugin_plan.clone # important that the plan is cloned -- we're going to add local plugins, and they shouldn't go into history
138
- @history["directives"] = @directives
147
+ info "Plan loaded. (#{@plugin_plan.size} plugins: " +
148
+ "#{@plugin_plan.map { |p| p['name'] }.join(', ')})" +
149
+ ". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}"
139
150
 
140
- info "Plan loaded. (#{@plugin_plan.size} plugins: " +
141
- "#{@plugin_plan.map { |p| p['name'] }.join(', ')})" +
142
- ". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}"
151
+ @new_plan = true # used in determination if we should checkin this time or not
143
152
 
144
- @new_plan = true # used in determination if we should checkin this time or not
145
- else
146
- info "There was a problem with plugin signatures. Reusing old plan."
147
- @plugin_plan = Array(@history["old_plugins"])
148
- @directives = @history["directives"] || Hash.new
149
- end
150
153
 
151
- # Add local plugins to the plan. Note that local plugins are NOT saved to history file
154
+ # Add local plugins to the plan.
152
155
  @plugin_plan += get_local_plugins
153
156
  rescue Exception =>e
154
157
  fatal "Plan from server was malformed: #{e.message} - #{e.backtrace}"
@@ -161,6 +164,7 @@ module Scout
161
164
  @plugin_plan += get_local_plugins
162
165
  @directives = @history["directives"] || Hash.new
163
166
  end
167
+ @plugin_plan.reject! { |p| p['code'].nil? }
164
168
  end
165
169
 
166
170
  # returns an array of hashes representing local plugins found on the filesystem
@@ -169,13 +173,20 @@ module Scout
169
173
  def get_local_plugins
170
174
  local_plugin_paths=Dir.glob(File.join(@local_plugin_path,"[a-zA-Z]*.rb"))
171
175
  local_plugin_paths.map do |plugin_path|
176
+ name = File.basename(plugin_path)
177
+ options = if directives = @plugin_plan.find { |plugin| plugin['filename'] == name }
178
+ directives['options']
179
+ else
180
+ nil
181
+ end
172
182
  begin
173
183
  {
174
- 'name' => File.basename(plugin_path),
175
- 'local_filename' => File.basename(plugin_path),
176
- 'origin' => 'LOCAL',
177
- 'code' => File.read(plugin_path),
178
- 'interval' => 0
184
+ 'name' => name,
185
+ 'local_filename' => name,
186
+ 'origin' => 'LOCAL',
187
+ 'code' => File.read(plugin_path),
188
+ 'interval' => 0,
189
+ 'options' => options
179
190
  }
180
191
  rescue => e
181
192
  info "Error trying to read local plugin: #{plugin_path} -- #{e.backtrace.join('\n')}"
@@ -193,6 +204,40 @@ module Scout
193
204
  def ping_key
194
205
  (@history['directives'] || {})['ping_key']
195
206
  end
207
+
208
+ # Returns the Scout public key for code verification.
209
+ def scout_public_key
210
+ return @scout_public_key if instance_variables.include?('@scout_public_key')
211
+ public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_id_rsa.pub] ))
212
+ debug "Loaded scout-wide public key used for verifying code signatures (#{public_key_text.size} bytes)"
213
+ @scout_public_key = OpenSSL::PKey::RSA.new(public_key_text)
214
+ end
215
+
216
+ # Returns the account-specific public key if installed. Otherwise, nil.
217
+ def account_public_key
218
+ return @account_public_key if instance_variables.include?('@account_public_key')
219
+ @account_public_key = nil
220
+ begin
221
+ public_key_text = File.read(@account_public_key_path)
222
+ debug "Loaded account public key used for verifying code signatures (#{public_key_text.size} bytes)"
223
+ @account_public_key=OpenSSL::PKey::RSA.new(public_key_text)
224
+ rescue Errno::ENOENT
225
+ debug "No account private key provided"
226
+ rescue
227
+ info "Error loading account public key: #{$!.message}"
228
+ end
229
+ return @account_public_key
230
+ end
231
+
232
+ # This is called in +run_plugins_by_plan+. When the agent starts its next run, it checks to see
233
+ # if the key has changed. If so, it forces a refresh.
234
+ def store_account_public_key
235
+ @history['account_public_key'] = account_public_key.to_s
236
+ end
237
+
238
+ def account_public_key_changed?
239
+ @history['account_public_key'] != account_public_key.to_s
240
+ end
196
241
 
197
242
  # uses values from history and current time to determine if we should checkin at this time
198
243
  def time_to_checkin?
@@ -245,9 +290,20 @@ module Scout
245
290
  end
246
291
  end
247
292
  take_snapshot if @directives['take_snapshots']
293
+ process_signature_errors
294
+ store_account_public_key
248
295
  checkin
249
296
  end
250
297
 
298
+ # Reports errors if there are any plugins with invalid signatures and sets a flag
299
+ # to force a fresh plan on the next run.
300
+ def process_signature_errors
301
+ return unless @plugins_with_signature_errors and @plugins_with_signature_errors.any?
302
+ @plugins_with_signature_errors.each do |plugin|
303
+ @checkin[:errors] << build_report(plugin,:subject => "Code Signature Error", :body => plugin['sig_error'])
304
+ end
305
+ end
306
+
251
307
  #
252
308
  # This is the heart of Scout.
253
309
  #
@@ -292,7 +348,7 @@ module Scout
292
348
  info "Plugin compiled."
293
349
  rescue Exception
294
350
  raise if $!.is_a? SystemExit
295
- error "Plugin would not compile: #{$!.message}"
351
+ error "Plugin #{plugin['path'] || plugin['name']} would not compile: #{$!.message}"
296
352
  @checkin[:errors] << build_report(plugin,:subject => "Plugin would not compile", :body=>"#{$!.message}\n\n#{$!.backtrace}")
297
353
  return
298
354
  end
@@ -310,7 +366,6 @@ module Scout
310
366
  end
311
367
  end
312
368
 
313
-
314
369
  debug "Loading plugin..."
315
370
  if job = Plugin.last_defined.load( last_run, (memory || Hash.new), options)
316
371
  info "Plugin loaded."
@@ -336,6 +391,7 @@ module Scout
336
391
  :subject => "Plugin failed to run",
337
392
  :body=>"#{$!.class}: #{$!.message}\n#{$!.backtrace.join("\n")}")
338
393
  end
394
+
339
395
  info "Plugin completed its run."
340
396
 
341
397
  %w[report alert error summary].each do |type|
@@ -350,6 +406,8 @@ module Scout
350
406
  end
351
407
  end
352
408
 
409
+ report_embedded_options(plugin,code_to_run)
410
+
353
411
  @history["last_runs"].delete(plugin['name'])
354
412
  @history["memory"].delete(plugin['name'])
355
413
  @history["last_runs"][id_and_name] = run_time
@@ -379,6 +437,22 @@ module Scout
379
437
  end
380
438
  info "Plugin '#{plugin['name']}' processing complete."
381
439
  end
440
+
441
+ # Adds embedded options to the checkin if the plugin is manually installed
442
+ # on this server.
443
+ def report_embedded_options(plugin,code)
444
+ return unless plugin['origin'] and Plugin.has_embedded_options?(code)
445
+ if options_yaml = Plugin.extract_options_yaml_from_code(code)
446
+ options=PluginOptions.from_yaml(options_yaml)
447
+ if options.error
448
+ debug "Problem parsing option definition in the plugin code:"
449
+ debug options_yaml
450
+ else
451
+ debug "Sending options to server"
452
+ @checkin[:options] << build_report(plugin,options.to_hash)
453
+ end
454
+ end
455
+ end
382
456
 
383
457
 
384
458
  # captures a list of processes running at this moment
@@ -393,13 +467,14 @@ module Scout
393
467
 
394
468
  # Prepares a check-in data structure to hold Plugin generated data.
395
469
  def prepare_checkin
396
- @checkin = { :reports => Array.new,
397
- :alerts => Array.new,
398
- :errors => Array.new,
399
- :summaries => Array.new,
400
- :snapshot => '',
401
- :config_path => File.expand_path(File.dirname(@history_file)),
402
- :server_name => @server_name}
470
+ @checkin = { :reports => Array.new,
471
+ :alerts => Array.new,
472
+ :errors => Array.new,
473
+ :summaries => Array.new,
474
+ :snapshot => '',
475
+ :config_path => File.expand_path(File.dirname(@history_file)),
476
+ :server_name => @server_name,
477
+ :options => Array.new}
403
478
  end
404
479
 
405
480
  def show_checkin(printer = :p)
@@ -522,6 +597,9 @@ module Scout
522
597
  end
523
598
 
524
599
  def checkin
600
+ debug """
601
+ #{PP.pp(@checkin, '')}
602
+ """
525
603
  @history['last_checkin'] = Time.now.to_i # might have to save the time of invocation and use here to prevent drift
526
604
  io = StringIO.new
527
605
  gzip = Zlib::GzipWriter.new(io)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout
3
3
  version: !ruby/object:Gem::Version
4
- hash: 63
5
- prerelease: false
4
+ hash: 61
5
+ prerelease:
6
6
  segments:
7
7
  - 5
8
8
  - 3
9
- - 2
10
- version: 5.3.2
9
+ - 3
10
+ version: 5.3.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Scout Monitoring
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-10 00:00:00 -07:00
18
+ date: 2011-06-28 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -51,6 +51,7 @@ extra_rdoc_files:
51
51
  files:
52
52
  - lib/scout/command/install.rb
53
53
  - lib/scout/command/run.rb
54
+ - lib/scout/command/sign.rb
54
55
  - lib/scout/command/test.rb
55
56
  - lib/scout/command/troubleshoot.rb
56
57
  - lib/scout/command.rb
@@ -221,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
222
  requirements: []
222
223
 
223
224
  rubyforge_project: scout
224
- rubygems_version: 1.3.7
225
+ rubygems_version: 1.4.2
225
226
  signing_key:
226
227
  specification_version: 3
227
228
  summary: Scout makes monitoring and reporting on your web applications as flexible and simple as possible.