# This file is part of Taskr. # # Taskr is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Taskr is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Taskr. If not, see . require 'rufus/scheduler' require 'date' class Rufus::Scheduler public :duration_to_f end module Taskr::Models class Task < Base has_many :task_actions, :include => :action_parameters, :dependent => :destroy serialize :schedule_options serialize :last_triggered_error validates_presence_of :schedule_method validates_presence_of :schedule_when validates_presence_of :name validates_uniqueness_of :name validates_presence_of :task_actions validates_associated :task_actions def schedule!(scheduler = Taskr.scheduler) case schedule_method when 'cron' method = :schedule when 'at' method = :schedule_at when 'in' method = :schedule_in when 'every' method = :schedule_every end if method == :schedule_at || method == :schedule_in t = next_trigger_time method = :schedule_at if t < Time.now $LOG.warn "Task #{name.inspect} will not be scheduled because its trigger time is in the past (#{t.inspect})." return nil end end $LOG.debug "Scheduling task #{name.inspect}: #{self.inspect}" if self.new_record? # Need to distinguish between the edit/create cases. "Edit" needs to reload the task_actions or nothing works; "Create" needs NOT to relaod the actions, or the validations kick in and nothing works. FIXME!!!!! if task_actions.length > 0 action = prepare_action else $LOG.warn "Task #{name.inspect} has no actions and as a result will not be scheduled!" return false end else if task_actions(true).length > 0 action = prepare_action else $LOG.warn "Task #{name.inspect} has no actions and as a result will not be scheduled!" return false end end job_id = scheduler.send(method, t || schedule_when, :schedulable => action) if job_id $LOG.debug "Task #{name.inspect} scheduled with job id #{job_id}" else $LOG.error "Task #{name.inspect} was NOT scheduled!" return nil end self.update_attribute(:scheduler_job_id, job_id) if method == :schedule_at || method == :schedule_in job = scheduler.get_job(job_id) at = job.schedule_info self.update_attribute(:schedule_when, at) self.update_attribute(:schedule_method, 'at') end return job_id end def prepare_action if task_actions.length == 1 ta = task_actions.first parameters = {} ta.action_parameters.each{|p| parameters[p.name] = p.value} action = (ta.action_class.kind_of?(Class) ? ta.action_class : ta.action_class.constantize).new(parameters) action.task = self action.task_action = ta elsif task_actions.length > 1 action = Taskr::Actions::Multi.new task_actions.each do |ta| parameters = {} ta.action_parameters.each{|p| parameters[p.name] = p.value} a = (ta.action_class.kind_of?(Class) ? ta.action_class : ta.action_class.constantize).new(parameters) a.task = self a.task_action = ta action.actions << a end action.task = self else raise "Task #{name.inspect} has no actions!" end action end def next_trigger_time # TODO: need to figure out how to calulate trigger_time for these.. for now return :unknown return :unknown unless schedule_method == 'at' || schedule_method == 'in' if schedule_method == 'in' return (created_on || Time.now) + Taskr.scheduler.duration_to_f(schedule_when) end # Time parsing code from Rails time_hash = Date._parse(schedule_when) time_hash[:sec_fraction] = ((time_hash[:sec_fraction].to_f % 1) * 1_000_000).to_i time_array = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction) # treat 0000-00-00 00:00:00 as nil Time.send(Base.default_timezone, *time_array) rescue DateTime.new(*time_array[0..5]) rescue nil end def to_s "#{name.inspect}@#{schedule_when}" end end class TaskAction < Base belongs_to :task has_many :action_parameters, :class_name => 'TaskActionParameter', :foreign_key => :task_action_id, :dependent => :destroy alias_method :parameters, :action_parameters has_many :log_entries has_one :last_log_entry, :class_name => 'LogEntry', :foreign_key => :task_id, :order => 'timestamp DESC' validates_associated :action_parameters def action_class=(class_name) if class_name.kind_of? Class self[:action_class_name] = class_name.to_s else self[:action_class_name] = class_name end end def action_class self[:action_class_name].constantize end def description action_class.description end def to_xml(options = {}) options[:indent] ||= 2 xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) xml.instruct! unless options[:skip_instruct] xml.tag!('task-action', :type => self.class) do xml.tag!('id', {:type => 'integer'}, id) xml.tag!('action-class-name', action_class_name) xml.tag!('order', {:type => 'integer'}, order) unless order.blank? xml.tag!('task-id', {:type => 'integer'}, task_id) xml.tag!('action-parameters', {:type => 'array'}) do action_parameters.each {|ap| ap.to_xml(options)} end end end def to_s "#{self.class.name.demodulize}(#{action_class})" end end class TaskActionParameter < Base belongs_to :task_action serialize :value def to_xml(options = {}) options[:indent] ||= 2 xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) xml.instruct! unless options[:skip_instruct] xml.tag!('action-parameter', :type => self.class) do xml.tag!('id', {:type => 'integer'}, id) xml.tag!('name', name) xml.tag!('value') do xml.cdata!(value.to_s) end end end def to_s "#{self.class.name.demodulize}(#{name}:#{value})" end end class LogEntry < Base belongs_to :task belongs_to :task_action class << self def log(level, action_or_task, data) level = level.upcase if action_or_task.kind_of? TaskAction action = action_or_task task = action.task elsif action_or_task.kind_of? Task action = nil task = action_or_task elsif action_or_task.kind_of? Integer action = TaskAction.find(action_or_task) task = action.task elsif action_or_task.kind_of? Taskr::Actions::Base action = action_or_task.task_action task = action.task else raise ArgumentError, "#{action_or_task.inspect} is not a valid Task or TaskAction!" end threshold = $CONF[:task_log][:level].upcase if $CONF[:task_log] && $CONF[:task_log][:level] if threshold.blank? || ['DEBUG', 'INFO', 'WARN', 'ERROR'].index(threshold) <= ['DEBUG', 'INFO', 'WARN', 'ERROR'].index(level) LogEntry.create( :level => level, :timestamp => Time.now, :task => task, :task_action => action, :data => data ) end # Send SNMP traps through net-snmp if Taskr is configured to send them. if $CONF[:snmp] && $CONF[:snmp][:send_traps] send_snmp_trap(level, task, action, data) end end # Produces a Logger-like class that will create log entries for the given # TaskAction. The returned object exploses behaviour much like a standard # Ruby Logger, so that it can be used in place of a Logger when necessary. def logger_for_action(action) ActionLogger.new(action) end ['debug', 'info', 'warn', 'error'].each do |level| define_method(level) do |action, data| log(level, action, data) end end def send_snmp_trap(level, task, action, data) snmp_community = $CONF[:snmp][:community] enterprise_oid = $CONF[:snmp][:enterprise_oid] || '1.3.6.1.4.1.55555.7007' to_host = $CONF[:snmp][:to_host] snmp_persistent_dir = $CONF[:snmp][:snmp_persistent_dir] || '/tmp' my_host = ENV['HOSTNAME'] || `hostname`.strip || 'taskr' task_oid = '1.3.6.1.4.1.55555.7007.1' task_typ = 's' task_val = task.to_s.gsub(/"/, '\"') # see http://www.oid-info.com/get/1.3.6.1.4.1.9.5.1.14.4.1.2 level_oid = '1.3.6.1.4.1.9.5.1.14.4.1.2' level_typ = 'i' case level when 'DEBUG' then level_val = 8 when 'INFO' then level_val = 7 when 'WARN' then level_val = 5 when 'ERROR' then level_val = 4 end # see http://www.oid-info.com/get/2.9.2.11.7.1 #repeat_oid = '2.9.2.11.7.1' #repeat_typ = 'i' #repeat_val = repeat_count # see http://www.oid-info.com/get/1.3.6.1.4.1.9.9.41.1.2.3.1.4 sevr_oid = '1.3.6.1.4.1.9.9.41.1.2.3.1.4' sevr_typ = 's' sevr_val = level # TODO: make msg format configurable msg = ("Taskr #{level} on task #{task}[#{action}]: #{data}").gsub(/"/, '\"') # see http://www.oid-info.com/get/1.3.6.1.4.1.9.9.41.1.2.3.1.5 msg_oid = '1.3.6.1.4.1.9.9.41.1.2.3.1.5' msg_typ = 's' msg_val = msg cmd = %{snmptrap -v 1 -c #{snmp_community} #{to_host} #{enterprise_oid} #{my_host} 6 #{level_val} '' \ #{level_oid} #{level_typ} "#{level_val}" \ #{sevr_oid} #{sevr_typ} "#{sevr_val}" \ #{task_oid} #{task_typ} "#{task_val}" \ #{msg_oid} #{msg_typ} "#{msg_val}"} # Band-aid fix for bug in Net-SNMP. # See http://sourceforge.net/tracker/index.php?func=detail&aid=1588455&group_id=12694&atid=112694 ENV['SNMP_PERSISTENT_DIR'] ||= snmp_persistent_dir $LOG.debug "SENDING SNMP TRAP: #{cmd}" `#{cmd}` end end # Exposes a Logger-like interface for logging entries for some particular # TaskAction. class ActionLogger def initialize(action) @action = action end def method_missing(method, data) LogEntry.send(method, @action, "#{"#{@progname}: " unless @progname.blank?}#{data}") end def respond_to?(method) [:debug, :info, :warn, :error].include?(method) end def progname action.task.name end def progname=(p) end end end class CreateTaskr < V 0.01 def self.up $LOG.info("Migrating database") create_table :taskr_tasks, :force => true do |t| t.column :name, :string, :null => false t.column :created_on, :timestamp, :null => false t.column :created_by, :string t.column :schedule_method, :string, :null => false t.column :schedule_when, :string, :null => false t.column :schedule_options, :text t.column :scheduler_job_id, :integer t.column :last_triggered, :datetime t.column :last_triggered_error, :text end add_index :taskr_tasks, [:name], :unique => true create_table :taskr_task_actions, :force => true do |t| t.column :task_id, :integer, :null => false t.column :action_class_name, :string, :null => false t.column :order, :integer end add_index :taskr_task_actions, [:task_id] create_table :taskr_task_action_parameters, :force => true do |t| t.column :task_action_id, :integer, :null => false t.column :name, :string, :null => false t.column :value, :text end add_index :taskr_task_action_parameters, [:task_action_id] add_index :taskr_task_action_parameters, [:task_action_id, :name] end def self.down drop_table :taskr_task_action_parameters drop_table :taskr_tasks end end class AddLoggingTables < V 0.3 def self.up $LOG.info("Creating logging tables") create_table :taskr_log_entries, :force => true do |t| t.column :task_id, :integer t.column :task_action_id, :integer t.column :timestamp, :timestamp, :null => false t.column :level, :string, :null => false t.column :data, :text end add_index :taskr_log_entries, :task_id add_index :taskr_log_entries, :task_action_id end def self.down drop_table :taskr_log_entries end end class AddMemoToTasks < V 0.3001 def self.up add_column :taskr_tasks, :memo, :text end def self.down remove_column :taskr_tasks, :memo end end class AddIndiciesToLogEntries < V 0.5 def self.up add_index :taskr_log_entries, :timestamp add_index :taskr_log_entries, :level end end end