scout 5.3.2 → 5.3.3

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