lib/scout/server.rb in scout-5.3.2 vs lib/scout/server.rb in scout-5.3.3

- old
+ new

@@ -47,14 +47,16 @@ @history_file = history_file @history = Hash.new @logger = logger @server_name = server_name @plugin_plan = [] + @plugins_with_signature_errors = [] @directives = {} # take_snapshots, interval, sleep_interval @new_plan = false @local_plugin_path = File.dirname(history_file) # just put overrides and ad-hoc plugins in same directory as history file. @plugin_config_path = File.join(@local_plugin_path, "plugins.properties") + @account_public_key_path = File.join(@local_plugin_path, "scout_rsa.pub") @plugin_config = load_plugin_configs(@plugin_config_path) # the block is only passed for install and test, since we split plan retrieval outside the lockfile for run if block_given? load_history @@ -62,11 +64,11 @@ save_history end end def refresh? - return true if !ping_key + return true if !ping_key or account_public_key_changed? # fetch the plan again if the account key is modified/created url=URI.join( @server.sub("https://","http://"), "/clients/#{ping_key}/ping.scout") headers = {"x-scout-tty" => ($stdin.tty? ? 'true' : 'false')} if @history["plan_last_modified"] and @history["old_plugins"] @@ -101,56 +103,57 @@ begin body = res.body if res["Content-Encoding"] == "gzip" and body and not body.empty? body = Zlib::GzipReader.new(StringIO.new(body)).read end - + body_as_hash = JSON.parse(body) - - # Ensure all the plugins in the new plan are properly signed. Load the public key for this. - public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_id_rsa.pub] )) - debug "Loaded public key used for verifying code signatures (#{public_key_text.size} bytes)" - code_public_key = OpenSSL::PKey::RSA.new(public_key_text) - + temp_plugins=Array(body_as_hash["plugins"]) - plugin_signature_error = false - temp_plugins.each do |plugin| + temp_plugins.each_with_index do |plugin,i| signature=plugin['signature'] id_and_name = "#{plugin['id']}-#{plugin['name']}".sub(/\A-/, "") if signature code=plugin['code'].gsub(/ +$/,'') # we strip trailing whitespace before calculating signatures. Same here. decoded_signature=Base64.decode64(signature) - if !code_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code) - info "#{id_and_name} signature doesn't match!" - plugin_signature_error=true + if !scout_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code) + if account_public_key + if !account_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code) + info "#{id_and_name} signature verification failed for both the Scout and account public keys" + 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." + @plugins_with_signature_errors << temp_plugins.delete_at(i) + end + else + info "#{id_and_name} signature doesn't match!" + plugin['sig_error'] = "The code signature failed verification. Please place your account-specific public key at #{@account_public_key_path}." + @plugins_with_signature_errors << temp_plugins.delete_at(i) + end end + # filename is set for local plugins. these don't have signatures. + elsif plugin['filename'] + plugin['code']=nil # should not have any code. else info "#{id_and_name} has no signature!" - plugin_signature_error=true + plugin['sig_error'] = "The code has no signature and cannot be verified." + @plugins_with_signature_errors << temp_plugins.delete_at(i) end end + @plugin_plan = temp_plugins + @directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new + @history["plan_last_modified"] = res["last-modified"] + @history["old_plugins"] = @plugin_plan + @history["directives"] = @directives - if(!plugin_signature_error) - @plugin_plan = temp_plugins - @directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new - @history["plan_last_modified"] = res["last-modified"] - @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 - @history["directives"] = @directives + info "Plan loaded. (#{@plugin_plan.size} plugins: " + + "#{@plugin_plan.map { |p| p['name'] }.join(', ')})" + + ". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}" - info "Plan loaded. (#{@plugin_plan.size} plugins: " + - "#{@plugin_plan.map { |p| p['name'] }.join(', ')})" + - ". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}" + @new_plan = true # used in determination if we should checkin this time or not - @new_plan = true # used in determination if we should checkin this time or not - else - info "There was a problem with plugin signatures. Reusing old plan." - @plugin_plan = Array(@history["old_plugins"]) - @directives = @history["directives"] || Hash.new - end - # Add local plugins to the plan. Note that local plugins are NOT saved to history file + # Add local plugins to the plan. @plugin_plan += get_local_plugins rescue Exception =>e fatal "Plan from server was malformed: #{e.message} - #{e.backtrace}" exit end @@ -159,25 +162,33 @@ info "Plan not modified." @plugin_plan = Array(@history["old_plugins"]) @plugin_plan += get_local_plugins @directives = @history["directives"] || Hash.new end + @plugin_plan.reject! { |p| p['code'].nil? } end # returns an array of hashes representing local plugins found on the filesystem # The glob pattern requires that filenames begin with a letter, # which excludes plugin overrides (like 12345.rb) def get_local_plugins local_plugin_paths=Dir.glob(File.join(@local_plugin_path,"[a-zA-Z]*.rb")) local_plugin_paths.map do |plugin_path| + name = File.basename(plugin_path) + options = if directives = @plugin_plan.find { |plugin| plugin['filename'] == name } + directives['options'] + else + nil + end begin { - 'name' => File.basename(plugin_path), - 'local_filename' => File.basename(plugin_path), - 'origin' => 'LOCAL', - 'code' => File.read(plugin_path), - 'interval' => 0 + 'name' => name, + 'local_filename' => name, + 'origin' => 'LOCAL', + 'code' => File.read(plugin_path), + 'interval' => 0, + 'options' => options } rescue => e info "Error trying to read local plugin: #{plugin_path} -- #{e.backtrace.join('\n')}" nil end @@ -191,10 +202,44 @@ end def ping_key (@history['directives'] || {})['ping_key'] end + + # Returns the Scout public key for code verification. + def scout_public_key + return @scout_public_key if instance_variables.include?('@scout_public_key') + public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_id_rsa.pub] )) + debug "Loaded scout-wide public key used for verifying code signatures (#{public_key_text.size} bytes)" + @scout_public_key = OpenSSL::PKey::RSA.new(public_key_text) + end + + # Returns the account-specific public key if installed. Otherwise, nil. + def account_public_key + return @account_public_key if instance_variables.include?('@account_public_key') + @account_public_key = nil + begin + public_key_text = File.read(@account_public_key_path) + debug "Loaded account public key used for verifying code signatures (#{public_key_text.size} bytes)" + @account_public_key=OpenSSL::PKey::RSA.new(public_key_text) + rescue Errno::ENOENT + debug "No account private key provided" + rescue + info "Error loading account public key: #{$!.message}" + end + return @account_public_key + end + + # This is called in +run_plugins_by_plan+. When the agent starts its next run, it checks to see + # if the key has changed. If so, it forces a refresh. + def store_account_public_key + @history['account_public_key'] = account_public_key.to_s + end + + def account_public_key_changed? + @history['account_public_key'] != account_public_key.to_s + end # uses values from history and current time to determine if we should checkin at this time def time_to_checkin? @history['last_checkin'] == nil || @directives['interval'] == nil || @@ -243,13 +288,24 @@ error("Encountered an error: #{$!.message}") puts $!.backtrace.join('\n') end end take_snapshot if @directives['take_snapshots'] + process_signature_errors + store_account_public_key checkin end + # Reports errors if there are any plugins with invalid signatures and sets a flag + # to force a fresh plan on the next run. + def process_signature_errors + return unless @plugins_with_signature_errors and @plugins_with_signature_errors.any? + @plugins_with_signature_errors.each do |plugin| + @checkin[:errors] << build_report(plugin,:subject => "Code Signature Error", :body => plugin['sig_error']) + end + end + # # This is the heart of Scout. # # First, it determines if a plugin is past interval and needs to be run. # If it is, it simply evals the code, compiling it. @@ -290,11 +346,11 @@ TOPLEVEL_BINDING, plugin['path'] || plugin['name'] ) info "Plugin compiled." rescue Exception raise if $!.is_a? SystemExit - error "Plugin would not compile: #{$!.message}" + error "Plugin #{plugin['path'] || plugin['name']} would not compile: #{$!.message}" @checkin[:errors] << build_report(plugin,:subject => "Plugin would not compile", :body=>"#{$!.message}\n\n#{$!.backtrace}") return end # Lookup any local options in plugin_config.properies as needed @@ -308,11 +364,10 @@ info "Plugin #{id_and_name}: option #{k} appears to be a lookup, but we can't find #{lookup_key} in #{@plugin_config_path}" end end end - debug "Loading plugin..." if job = Plugin.last_defined.load( last_run, (memory || Hash.new), options) info "Plugin loaded." debug "Running plugin..." begin @@ -334,10 +389,11 @@ "#{$!.backtrace.join("\n")}" @checkin[:errors] << build_report(plugin, :subject => "Plugin failed to run", :body=>"#{$!.class}: #{$!.message}\n#{$!.backtrace.join("\n")}") end + info "Plugin completed its run." %w[report alert error summary].each do |type| plural = "#{type}s".sub(/ys\z/, "ies").to_sym reports = data[plural].is_a?(Array) ? data[plural] : @@ -348,10 +404,12 @@ reports.each do |fields| @checkin[plural] << build_report(plugin, fields) end end + report_embedded_options(plugin,code_to_run) + @history["last_runs"].delete(plugin['name']) @history["memory"].delete(plugin['name']) @history["last_runs"][id_and_name] = run_time @history["memory"][id_and_name] = data[:memory] else @@ -377,10 +435,26 @@ error "Unable to remove plugin." end end info "Plugin '#{plugin['name']}' processing complete." end + + # Adds embedded options to the checkin if the plugin is manually installed + # on this server. + def report_embedded_options(plugin,code) + return unless plugin['origin'] and Plugin.has_embedded_options?(code) + if options_yaml = Plugin.extract_options_yaml_from_code(code) + options=PluginOptions.from_yaml(options_yaml) + if options.error + debug "Problem parsing option definition in the plugin code:" + debug options_yaml + else + debug "Sending options to server" + @checkin[:options] << build_report(plugin,options.to_hash) + end + end + end # captures a list of processes running at this moment def take_snapshot info "Taking a process snapshot" @@ -391,17 +465,18 @@ return nil end # Prepares a check-in data structure to hold Plugin generated data. def prepare_checkin - @checkin = { :reports => Array.new, - :alerts => Array.new, - :errors => Array.new, - :summaries => Array.new, - :snapshot => '', - :config_path => File.expand_path(File.dirname(@history_file)), - :server_name => @server_name} + @checkin = { :reports => Array.new, + :alerts => Array.new, + :errors => Array.new, + :summaries => Array.new, + :snapshot => '', + :config_path => File.expand_path(File.dirname(@history_file)), + :server_name => @server_name, + :options => Array.new} end def show_checkin(printer = :p) send(printer, @checkin) end @@ -520,9 +595,12 @@ fatal "An HTTP error occurred: #{$!.message}" exit end def checkin + debug """ +#{PP.pp(@checkin, '')} + """ @history['last_checkin'] = Time.now.to_i # might have to save the time of invocation and use here to prevent drift io = StringIO.new gzip = Zlib::GzipWriter.new(io) gzip << @checkin.to_json gzip.close