require 'yaml' require 'open-uri' require 'cgi' require "socket" def eval_and_fetch_constants(x) old = Module.constants.map{|c| c.to_s} eval(x) new = (Module.constants.map{|c| c.to_s} - old) new = new.select{|c| c.to_s =~ /^TEMP/ } # do not fetch required libs, just user-defined new.map{|c| const_get(c) } end class Scout class Plugin OPTIONS = {}.to_yaml def self.clean_class_name parts = to_s.split('::') parts.size == 1 ? parts.first : parts[1..-1].join('::') end protected def report(metrics) metrics.each do |key, value| Deputy.send_report "#{self.class.clean_class_name}.#{key}", value end end def error(*args) report :error => args.map{|a| a.inspect}.join(', ') end def alert(*args) report :alert => args.map{|a| a.inspect}.join(', ') end def memory(key) complete_memory[key] end def remember(*args) data = (args.size == 1 ? args[0] : {args[0] => args[1]}) all = complete_memory.merge(data) File.open(memory_file, 'w'){|f| f.write all.to_yaml } end def self.needs(*libs) libs.each{|lib| require lib } end private def complete_memory return {} unless File.exist?(memory_file) YAML.load(File.read(memory_file)) || {} end def memory_file "/tmp/deputy.memory.#{self.class.clean_class_name}.yml" end # stub options for now... def option(key) (YAML.load(self.class::OPTIONS)[key.to_s]||{})['default'] end end def self.plugins(code) eval_and_fetch_constants(code).map do |container| interval = container.interval unless plugin = plugin_in_container(container) Deputy.send_report "Deputies.Plugin not found", code next end [interval, plugin] end.compact end def self.plugin_in_container(container) constants = container.constants.map{|constant_name|container.const_get(constant_name)} constants.detect{|c| c.instance_methods.map{|m| m.to_s}.include?('build_report') } end end module Deputy VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip DEFAULT_VALUE = 'OK' def self.install_cron executable = `which deputy`.strip unless (`crontab -l`).include?(executable) `crontab -l | { cat; echo "* * * * * #{executable} --run-plugins >> /tmp/deputy.log 2>&1"; } | crontab -` if executable !~ %r{^/usr/} puts "make deputy globally available! or e.g. calls from inside cronjobs do not know deputy" puts "sudo ln -s #{executable} /usr/bin/deputy" end end end def self.run_plugins(options={}) start_time = Time.now.to_i sleep_random_interval unless options[:no_wait] content = get("/plugins.rb") exceptions = [] Scout.plugins(content).each do |interval, plugin| wait = minutes_to_wait(start_time, interval) if wait == 0 puts "#{plugin.clean_class_name}: running" begin plugin.new.build_report rescue Object => e # catch and report plugin-specific errors e.message[0..0] = plugin.clean_class_name puts e exceptions << e end else puts "#{plugin.clean_class_name}: waiting another #{wait} minutes" end end send_report 'Deputies.finished', (exceptions.empty? ? DEFAULT_VALUE : 'Error') raise exceptions.first unless exceptions.empty? rescue Object => e # catch and report neta errors send_report "Deputies.Error", e.message raise e end def self.send_report(group, value, options = {}) return if config['disabled'] raise "separate #{group} with a ." unless group.split('.',2).size == 2 get "/notify?group=#{CGI.escape group}&value=#{CGI.escape value.to_s}", options end def self.get(path, options = {}) url = "#{sheriff_url}#{path}" url = "http://#{url}" unless url =~ %r{://} options[:http_basic_authentication] = extract_auth_from_url!(url) url = add_host_to_url(url, options.delete(:host)) Timeout.timeout(config['timeout']||10) do open(url, options).read end rescue Exception => e e.message << url.to_s raise e end def self.sheriff_url config['sheriff_url'].sub(%r{/$},'') end def self.config home = File.expand_path('~') ["#{home}/.deputy.yml", '/etc/deputy.yml'].each do |file| return YAML.load(File.read(file)) if File.exist?(file) end raise "No deputy.yml found in /etc or #{home}" end def self.minutes_to_wait(start_time, interval) start_minute = start_time / 60 run_every_n_minutes = interval / 60 start_minute % run_every_n_minutes end def self.sleep_random_interval if max = config['max_random_start_delay'] constant_number = Socket.gethostname.sum{|x| x[0]} sleep seeded_random(max, constant_number) end end def self.seeded_random(max_rand, seed) old = srand(seed) result = rand(max_rand) srand(old) result end # stolen from klarlack -- http://github.com/schoefmax/klarlack # to get an reliable timeout that wont fail on other platforms # or if sytem_timer is missing Timeout = begin # Try to use the SystemTimer gem instead of Ruby's timeout library # when running on something that looks like Ruby 1.8.x. See: # http://ph7spot.com/articles/system_timer # We don't want to bother trying to load SystemTimer on jruby and # ruby 1.9+. if RUBY_VERSION =~ /^1\.8\./ and RUBY_PLATFORM !~ /java/ require 'system_timer' SystemTimer else require 'timeout' Timeout end rescue LoadError => e $stderr.puts "Could not load SystemTimer gem, falling back to Ruby's slower/unsafe timeout library: #{e.message}" require 'timeout' Timeout end def self.extract_auth_from_url!(url) url.sub!(%r{//(.*?):(.*?)@}, '//') auth = [$1, $2].compact auth.empty? ? nil : auth end def self.add_host_to_url(url, host=nil) query = "hostname=#{host || Socket.gethostname}#{'&forced_host=true' if host}" separator = (url.include?('?') ? "&" : "?") url + separator + query end end