From f6cd51a9628bf6f876a3f2bb080f707b22e3e942 Mon Sep 17 00:00:00 2001 From: Jonathan Hoyt Date: Tue, 23 Dec 2008 17:13:22 -0500 Subject: [PATCH] delete, update, and add sentries works, pulling sentries configs via json with http auth works, sentry.survey! works --- app/controllers/clients_controller.rb | 8 +- app/controllers/devices_controller.rb | 2 +- app/controllers/sentries_controller.rb | 22 +++- app/models/event.rb | 12 +- app/models/sentry.rb | 9 ++ app/views/layouts/clients.html.erb | 3 - config/environment.rb | 3 + db/migrate/20081212174923_create_sentries.rb | 2 +- lib/daemon.rb | 128 +++++++++++++++++++ lib/statuslang.rb | 6 + lib/statuslang/lang.rb | 43 +++++++ lib/statuslang/message.rb | 60 +++++++++ lib/statuslang/ruby_lang_extensions.rb | 38 ++++++ 13 files changed, 327 insertions(+), 9 deletions(-) create mode 100644 lib/daemon.rb create mode 100644 lib/statuslang.rb create mode 100644 lib/statuslang/lang.rb create mode 100644 lib/statuslang/message.rb create mode 100644 lib/statuslang/ruby_lang_extensions.rb diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb index 3156849..a34df2b 100644 --- a/app/controllers/clients_controller.rb +++ b/app/controllers/clients_controller.rb @@ -3,7 +3,7 @@ class ClientsController < ApplicationController layout 'clients' def index - @clients = Client.find(:all, :order => :updated_at, :limit => 100) + @clients = [] respond_to do |format| format.html # index.html.erb @@ -13,7 +13,11 @@ def index end def search - @clients = Client.search(params[:q], :limit => 100, :only => [:firstname, :lastname, :name]) + if !params[:q].blank? + @clients = Client.search(params[:q], :limit => 100, :only => [:firstname, :lastname, :name]) + else + @clients = [] + end respond_to do |format| format.xml { render :xml => @clients } format.json { render :json => @clients } diff --git a/app/controllers/devices_controller.rb b/app/controllers/devices_controller.rb index 7e1128f..bede3a5 100644 --- a/app/controllers/devices_controller.rb +++ b/app/controllers/devices_controller.rb @@ -13,7 +13,7 @@ def index respond_to do |format| format.html format.xml { render :xml => @devices } - format.json { render :json => @devices } + format.json { render :json => @devices.to_json() } end end diff --git a/app/controllers/sentries_controller.rb b/app/controllers/sentries_controller.rb index b80e1ab..b438ee7 100644 --- a/app/controllers/sentries_controller.rb +++ b/app/controllers/sentries_controller.rb @@ -1,7 +1,17 @@ class SentriesController < ApplicationController - before_filter :login_required + before_filter :login_required, :except => 'index' + before_filter :http_basic_authenticate, :only => 'index' layout nil + def index + if params[:device_id] + @sentries = Sentry.find(:all, :conditions => {:device_id => params[:device_id]}) + respond_to do |format| + format.json { render :json => @sentries } + end + end + end + def edit @sentry = Sentry.find(params[:id]) end @@ -33,4 +43,14 @@ def destroy @sentry.destroy redirect_to :back end + + protected + + def http_basic_authenticate + authenticate_or_request_with_http_basic do |username, password| + # after testing uncomment the following line and comment out the test line + # username == APP_CONFIG[:event_api_username] && password == APP_CONFIG[:event_api_password] + username == "test" && password == "test" + end + end end \ No newline at end of file diff --git a/app/models/event.rb b/app/models/event.rb index f1c414d..3a15b54 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,3 +1,13 @@ class Event < ActiveRecord::Base belongs_to :recordable, :polymorphic => true -end + + def has(word) + self.message =~ /#{word}/ ? true : false + end + alias :have :has + + def message + self.data + end + +end \ No newline at end of file diff --git a/app/models/sentry.rb b/app/models/sentry.rb index ec266dd..79d07ab 100644 --- a/app/models/sentry.rb +++ b/app/models/sentry.rb @@ -2,5 +2,14 @@ class Sentry < ActiveRecord::Base has_many :events, :as => :recordable, :dependent => :destroy belongs_to :device belongs_to :schedule + belongs_to :goggle + + def survey! + StatusLang.run(self.id, self.goggle.script) ? true : false + end + + def notify!(message=nil) + NotificationQueue.create(:message => message, :schedule_id => self.schedule_id) + end end diff --git a/app/views/layouts/clients.html.erb b/app/views/layouts/clients.html.erb index 273ac4a..a67a60d 100644 --- a/app/views/layouts/clients.html.erb +++ b/app/views/layouts/clients.html.erb @@ -27,9 +27,6 @@

Clients

diff --git a/config/environment.rb b/config/environment.rb index 8f162be..5d58295 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -10,6 +10,8 @@ # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') require 'redcloth' +require 'days_and_times' +# require 'json/pure' Rails::Initializer.run do |config| # Settings in config/environments/* take precedence over those specified here. @@ -70,4 +72,5 @@ config.gem 'json' config.gem 'paperclip' require 'lib/search.rb' + require 'lib/statuslang.rb' end diff --git a/db/migrate/20081212174923_create_sentries.rb b/db/migrate/20081212174923_create_sentries.rb index da53018..1135829 100644 --- a/db/migrate/20081212174923_create_sentries.rb +++ b/db/migrate/20081212174923_create_sentries.rb @@ -4,7 +4,7 @@ def self.up t.boolean :state t.text :message t.integer :device_id - t.string :goggle_parameters + t.string :parameters t.datetime :last_surveyed_at t.integer :survey_interval t.integer :notifications_to_send diff --git a/lib/daemon.rb b/lib/daemon.rb new file mode 100644 index 0000000..72a6ff5 --- /dev/null +++ b/lib/daemon.rb @@ -0,0 +1,128 @@ +# Loop. +# Do any survey that either doesn't have a last_surveyed_at, or is past last_surveyed_at + survey_interval. And is not currently ignored. +# Record last_surveyed_at +# Notify if haven't notified too recently +# Record last_notified_at +# +# Later: Partition groups of surveys into separate threads to get it done faster. +require 'fileutils' + +def loggit!(msg) + File.open("log/watcher.log", 'a') do |log| + log << "[#{Time.now.strftime("%D %T")}] #{msg}\n" + end + msg +end +def store_pid(pid) + File.open("log/watcher.pid", 'w'){|f| f.write("#{pid}\n")} +end +def delete_pidfile + FileUtils.rm("log/watcher.pid") +end + +def watch! + # This will naturally migrate different surveys to need run slightly apart from one another. + while(sleep(1)) + Sentry.find(:all).each do |sentry| + begin + if (sentry.last_surveyed_at.nil? || Time.now > sentry.last_surveyed_at.to_time + sentry.survey_interval) + begin + success = sentry.survey! + if success + loggit! "SUCCESS #{sentry.device.name} / #{sentry.goggle.name}" + sentry.last_notified_at = nil + else !success + loggit! "FAILURE #{sentry.device.name} / #{sentry.goggle.name}" + if sentry.last_notified_at.nil? || (sentry.notifications_to_send > 1 && Time.now > sentry.last_notified_at.to_time + sentry.maximum_notify_frequency) + if sentry.notify! + sentry.last_notified_at = Time.now + loggit! " -> NOTIFIED" + else + loggit! " -> Could Not Notify" + end + end + end + rescue => e + STDERR << loggit!("ERROR - (#{sentry.device.name} / #{sentry.goggle.name}): #{e}") + if sentry.last_notified_at.nil? || (sentry.notifications_to_send > 1 && Time.now > sentry.last_notified_at.to_time + sentry.maximum_notify_frequency) + loggit! " -> NOTIFIED of ERROR" + sentry.last_notified_at = Time.now if sentry.notify!("Sentry #{sentry.device.name} / #{sentry.goggle.name} errored: #{e}") + end + ensure + sentry.last_surveyed_at = Time.now + sentry.save + end + end + rescue => e + STDERR << loggit!("ERROR - Was going to cause exit on Sentry #{sentry.device.name}! Error: #{e}") + sentry.notify!("Sentry #{sentry.device.name} Exit-causing error: #{e}", 'dcparker@gmail.com') + end + end + end +end + +watch! + +# +# def start_daemon! +# if File.exists?("log/watcher.pid") +# pid = IO.read("log/watcher.pid").chomp.to_i +# puts "Already running on process #{pid}!" +# else +# fork do +# Process.setsid +# exit if fork +# at_exit { +# delete_pidfile +# loggit!('Daemon Stopped') +# } +# File.umask 0000 +# STDIN.reopen "/dev/null" +# trap("TERM") { exit } +# trap("INT") { exit } +# store_pid(Process.pid) +# puts "Started Watcher Daemon on process #{Process.pid}." +# STDOUT.reopen "/dev/null", "a" +# STDERR.reopen "/dev/null" +# loggit!('Daemon Starting...') +# STDERR.reopen(File.open("log/watcher_errors.log", 'a')) +# Merb::Config[:environment] = 'production' unless ARGV[1] == 'development' || Merb::Config[:environment] +# ::Merb::BootLoader.initialize_merb +# loggit!(' -> Started') +# begin +# watch! +# rescue => e +# STDERR << loggit!("Program error OUTSIDE loop! > #{e}") +# email = Merb::Mailer.new(:to => 'dcparker@gmail.com', +# :from => "imonit@sabretechllc.com", +# :subject => "iMonit Daemon Stopped on ERROR", +# :text => "Program error OUTSIDE loop! > #{e}") +# email.deliver! +# end +# end +# end +# end +# +# def stop_daemon! +# begin +# pid = IO.read("log/watcher.pid").chomp.to_i +# Process.kill("INT", pid) +# loggit! "Daemon Stopped Manually :)" +# puts "Stopped Watcher Daemon on process #{pid}" +# rescue => k +# puts "Failed to kill! #{k}" +# ensure +# exit +# end +# end +# +# case ARGV[0] +# when 'start' +# start_daemon! +# when 'stop' +# stop_daemon! +# when 'log' +# puts IO.read("log/watcher.log") +# else +# puts "Usage: ruby lib/daemon.rb start|stop|log" +# end diff --git a/lib/statuslang.rb b/lib/statuslang.rb new file mode 100644 index 0000000..5db684b --- /dev/null +++ b/lib/statuslang.rb @@ -0,0 +1,6 @@ +require 'statuslang/lang' +module StatusLang + def self.run(sentry_id,code) + Lang.new(sentry_id).instance_eval(code) + end +end diff --git a/lib/statuslang/lang.rb b/lib/statuslang/lang.rb new file mode 100644 index 0000000..0565a20 --- /dev/null +++ b/lib/statuslang/lang.rb @@ -0,0 +1,43 @@ +require 'statuslang/ruby_lang_extensions' +module StatusLang + class Lang + def initialize(sentry_id) + @sentry_id = sentry_id + end + + def last(amount=nil) + if amount + if amount.is_a?(Duration) + options[:after] = amount.ago + elsif amount.is_a?(Integer) + options[:limit] = amount + else + raise ArgumentError, "amount must be a Duration or an Integer!" + end + end + self + end + + def posts + if @options[:after] + Event.find(:all, :conditions => {:recordable_type => "Sentry", :recordable_id => @sentry_id, :created_at.gte => @options[:after]}) + elsif @options[:limit] + Event.find(:all, :conditions => {:recordable_type => "Sentry", :recordable_id => @sentry_id}, :limit => @options[:limit]) + end + end + def post + Event.find(:last, :conditions => {:recordable_type => "Sentry", :recordable_id => @sentry_id}) + end + def all_posts + posts.all + end + def any_post + posts.any + end + + private + def options + @options ||= {} + end + end +end diff --git a/lib/statuslang/message.rb b/lib/statuslang/message.rb new file mode 100644 index 0000000..05dddaa --- /dev/null +++ b/lib/statuslang/message.rb @@ -0,0 +1,60 @@ +require 'simple_mapper' +require 'net/http' +require 'lib/nethttp_magic' + +module StatusLang + # Post is a String class that is also a SimpleMapper persistence class reading from StatusPing.com. + # The reason it is also a string is so that it can be treated as a simple string of the Message content itself if desired. + # + # To use, first set the options (which will be set per-feed) + # StatusLang::Post.options(:feed_key => feed_key, :feed_secret => feed_secret) + # then call just like any SimpleMapper model: Post.get(..query-params..). + class Message < String + include SimpleMapper::Persistence + set_format :json + set_entity_name 'message' + add_connection_adapter(:http) do + set_base_url 'http://statusping.com/messages' + end + has :properties + property :feed + property :created_by + property :content + property :created_at + uses :callbacks + add_callback('initialize_request') do |request,cboptions| + # Add HTTP Basic Auth to the request + # the nonce can technically be anything, but to be best secure we want it pretty randomly generated. + nonce = Digest::SHA1.hexdigest("--#{rand(1234)}--#{Time.now.to_s}--#{rand(12345)}--") + sequence = Time.now.to_f + request.add_query_param(:nonce => nonce) + request.basic_auth options[:feed_key], Digest::SHA1.hexdigest("#{nonce}#{options[:feed_secret]}") + [request,cboptions] + end + def self.options(options=nil) + Thread.current['statusping_options'] = options if options + Thread.current['statusping_options'] ||= {} + end + + # Sets the data into the object. This is provided as a default method, but your model can overwrite it any + # way you want. For example, you could set the data to some other object type, or to a Marshalled storage. + # The type of data you receive will depend on the format and parser you use. Of course you could make up + # your own spin-off of one of those, too. + def data=(data) + raise TypeError, "data must be a hash" unless data.is_a?(Hash) + data.each {|k,v| instance_variable_set("@#{k}", v)} + self.replace(@content) + end + alias :update_data :data= + + + # The following is for StatusLang. + + attr_accessor :any_all + + def has(word) + self =~ /#{word}/ ? true : false + end + alias :have :has + end +end diff --git a/lib/statuslang/ruby_lang_extensions.rb b/lib/statuslang/ruby_lang_extensions.rb new file mode 100644 index 0000000..d4904a0 --- /dev/null +++ b/lib/statuslang/ruby_lang_extensions.rb @@ -0,0 +1,38 @@ +class Array + def not_empty + !empty? + end + + def all + @any_all = :all + self + end + def any + @any_all = :any + self + end + + # Very special magic! If you have an array of objects, you can call a method on the array and it will be passed to each element, IF all elements respond to that method. + def method_missing(sym, *args) + # If all elements respond to the method, call it on each of them and return an array of the results. + if self.all? {|e| e.respond_to?(sym)} + if @any_all + if @any_all == :all + return self.all? {|e| e.send(sym, *args)} + elsif @any_all == :any + return self.any? {|e| e.send(sym, *args)} + else + raise "@any_all must be only :any or :all" + end + else + return self.collect {|e| e.send(sym, *args)} + end + end + # To force calling the method even if method_missing doesn't show they all work, or otherwise just for readability, use this instead: + # [].collect_somethings == [].collect(&:something) + return self.collect {|e| e.send(Inflector.singularize(sym.to_s.match(/^collect_(.*)/)[1]).to_sym, *args)} if sym.to_s =~ /^collect_(.*)/ + super + end + + alias :count :length +end