#!/usr/bin/env ruby require 'chronic' require 'chronic_duration' require 'sinatra/base' require 'tilt/erb' require 'uri' require 'flapjack/gateways/web/middleware/request_timestamp' require 'flapjack-diner' require 'flapjack/utility' module Flapjack module Gateways class Web < Sinatra::Base set :root, File.dirname(__FILE__) use Flapjack::Gateways::Web::Middleware::RequestTimestamp use Rack::MethodOverride set :sessions, :true set :raise_errors, false set :protection, except: :path_traversal set :views, settings.root + '/web/views' set :public_folder, settings.root + '/web/public' set :erb, :layout => 'layout.html'.to_sym class << self def start Flapjack.logger.info "starting web - class" set :show_exceptions, false @show_exceptions = Sinatra::ShowExceptions.new(self) if access_log = (@config && @config['access_log']) unless File.directory?(File.dirname(access_log)) raise "Parent directory for log file #{access_log} doesn't exist" end use Rack::CommonLogger, ::Logger.new(@config['access_log']) end # session's only used for error message display, so session_secret = @config['session_secret'] use Rack::Session::Cookie, :key => 'flapjack.session', :path => '/', :secret => session_secret || SecureRandom.hex(64) @api_url = @config['api_url'] if @api_url.nil? raise "'api_url' config must contain a Flapjack API instance address" end uri = begin URI(@api_url) rescue URI::InvalidURIError # TODO should we just log and re-raise the exception? raise "'api_url' is not a valid URI (#{@api_url})" end unless ['http', 'https'].include?(uri.scheme) raise "'api_url' is not a valid http or https URI (#{@api_url})" end unless @api_url.match(/^.*\/$/) Flapjack.logger.info "api_url must end with a trailing '/', setting to '#{@api_url}/'" @api_url = "#{@api_url}/" end Flapjack::Diner.base_uri(@api_url) Flapjack::Diner.logger = Flapjack.logger # constants won't be exposed to eRb scope @default_logo_url = "img/flapjack-2013-notext-transparent-300-300.png" @logo_image_file = nil @logo_image_ext = nil if logo_image_path = @config['logo_image_path'] if File.file?(logo_image_path) @logo_image_file = logo_image_path @logo_image_ext = File.extname(logo_image_path) else Flapjack.logger.error "logo_image_path '#{logo_image_path}'' does not point to a valid file." end end @auto_refresh = (@config['auto_refresh'].respond_to?('to_i') && (@config['auto_refresh'].to_i > 0)) ? @config['auto_refresh'].to_i : false end end include Flapjack::Utility helpers do def h(text) ERB::Util.h(text) end def u(text) ERB::Util.u(text) end def include_active?(path) return '' unless request.path == "/#{path}" " class='active'" end def charset_for_content_type(ct) charset = Encoding.default_external charset.nil? ? ct : "#{ct}; charset=#{charset.name}" end end ['config'].each do |class_inst_var| define_method(class_inst_var.to_sym) do self.class.instance_variable_get("@#{class_inst_var}") end end before do content_type charset_for_content_type('text/html') # needs to be done per-thread Flapjack.configure_log('web', config['logger']) @base_url = "#{request.base_url}/" @default_logo_url = self.class.instance_variable_get('@default_logo_url') @logo_image_file = self.class.instance_variable_get('@logo_image_file') @logo_image_ext = self.class.instance_variable_get('@logo_image_ext') @auto_refresh = self.class.instance_variable_get('@auto_refresh') input = nil query_string = (request.query_string.respond_to?(:length) && request.query_string.length > 0) ? "?#{request.query_string}" : "" if Flapjack.logger.debug? input = env['rack.input'].read Flapjack.logger.debug("#{request.request_method} #{request.path_info}#{query_string} #{input}") elsif Flapjack.logger.info? input = env['rack.input'].read input_short = input.gsub(/\n/, '').gsub(/\s+/, ' ') Flapjack.logger.info("#{request.request_method} #{request.path_info}#{query_string} #{input_short[0..80]}") end env['rack.input'].rewind unless input.nil? end get '/img/branding.*' do halt(404) unless @logo_image_file && params[:splat].first.eql?(@logo_image_ext[1..-1]) send_file(@logo_image_file) end get '/' do @metrics = Flapjack::Diner.metrics erb 'index.html'.to_sym end get '/self_stats' do @current_time = Time.now @api_url = self.class.instance_variable_get('@api_url') @metrics = Flapjack::Diner.metrics statistics = Flapjack::Diner.statistics unless statistics.nil? @executive_instances = statistics.each_with_object({}) do |stats, memo| if 'global'.eql?(stats[:instance_name]) @global_stats = stats next end boot_time = Time.parse(stats[:created_at]) uptime = @current_time - boot_time uptime_string = ChronicDuration.output(uptime, :format => :short, :keep_zero => true, :units => 2) || '0s' event_counters = {} event_rates = {} [:all_events, :ok_events, :failure_events, :action_events, :invalid_events].each do |evt| count = stats[evt] event_counters[evt] = count event_rates[evt] = (uptime > 0) ? (count.to_f / uptime).round : nil end memo[stats[:instance_name]] = { :uptime => uptime, :uptime_string => uptime_string, :event_counters => event_counters, :event_rates => event_rates } end end erb 'self_stats.html'.to_sym end get '/tags' do opts = {} @name = params[:name] opts.update(:name => @name) unless @name.nil? || @name.empty? @tags = Flapjack::Diner.tags(:filter => opts, :page => (params[:page] || 1)) unless @tags.nil? || @tags.empty? @pagination = pagination_from_context(Flapjack::Diner.context) unless @pagination.nil? @links = create_pagination_links(@pagination[:page], @pagination[:total_pages]) end end erb 'tags.html'.to_sym end get '/tags/:id' do tag_id = params[:id] @tag = Flapjack::Diner.tags(tag_id, :include => 'checks') err(404, "Could not find tag '#{tag_id}'") if @tag.nil? @checks = Flapjack::Diner.related(@tag, :checks) erb 'tag.html'.to_sym end get '/checks' do time = Time.now opts = {} @name = params[:name] opts.update(:name => @name) unless @name.nil? || @name.empty? @enabled = boolean_from_str(params[:enabled]) opts.update(:enabled => @enabled) unless @enabled.nil? @failing = boolean_from_str(params[:failing]) opts.update(:failing => @failing) unless @failing.nil? @checks = Flapjack::Diner.checks(:filter => opts, :page => (params[:page] || 1), :include => ['current_state', 'latest_notifications', 'current_scheduled_maintenances', 'current_unscheduled_maintenance']) @states = {} unless @checks.nil? || @checks.empty? @pagination = pagination_from_context(Flapjack::Diner.context) unless @pagination.nil? @links = create_pagination_links(@pagination[:page], @pagination[:total_pages]) end @states = @checks.each_with_object({}) do |check, memo| memo[check[:id]] = check_state(check, time) end end erb 'checks.html'.to_sym end get '/checks/:id' do check_id = params[:id] @current_time = DateTime.now # contacts.media will also return contacts, per JSONAPI v1 relationships @check = Flapjack::Diner.checks(check_id, :include => ['contacts.media', 'current_state', 'latest_notifications', 'current_scheduled_maintenances', 'current_unscheduled_maintenance']) halt(404, "Could not find check '#{check_id}'") if @check.nil? @contacts = [] @media_by_contact_id = {} @state = check_extra_state(@check, @current_time) @contacts = Flapjack::Diner.related(@check, :contacts) @media_by_contact_id = @contacts.inject({}) do |memo, contact| memo[contact[:id]] = Flapjack::Diner.related(contact, :media) memo end # these two requests will only get first page of 20 records, which is what we want state_links = Flapjack::Diner.check_link_states(check_id, :include => 'states') incl = Flapjack::Diner.included_data unless incl.nil? @state_changes = incl['state'].nil? ? [] : incl['state'].values end sm_links = Flapjack::Diner.check_link_scheduled_maintenances(check_id, :include => 'scheduled_maintenances') incl = Flapjack::Diner.included_data unless incl.nil? @scheduled_maintenances = incl['scheduled_maintenance'].nil? ? [] : incl['scheduled_maintenance'].values end @error = session[:error]; session.delete(:error) erb 'check.html'.to_sym end post "/acknowledgements" do summary = params[:summary] check_id = params[:check_id] dur = ChronicDuration.parse(params[:duration] || '') duration = (dur.nil? || (dur <= 0)) ? (4 * 60 * 60) : dur # FIXME create with known id, poll a few times and return # success/failure in session -- or maybe some AJAX method to # show success/failure? Flapjack::Diner.create_acknowledgements(:summary => summary, :duration => duration, :check => check_id) err = Flapjack::Diner.error unless err.nil? session[:error] = "Could not create the acknowledgement: #{err}" end redirect back end patch '/unscheduled_maintenances/:id' do unscheduled_maintenance_id = params[:id] Flapjack::Diner.update_unscheduled_maintenances( :id => unscheduled_maintenance_id, :end_time => Time.now) err = Flapjack::Diner.error unless err.nil? session[:error] = "Could not end unscheduled maintenance: #{err}" end redirect back end post '/scheduled_maintenances' do check_id = params[:check_id] start_time = Chronic.parse(params[:start_time]) raise ArgumentError, "start time parsed to nil" if start_time.nil? duration = ChronicDuration.parse(params[:duration]) summary = params[:summary] Flapjack::Diner.create_scheduled_maintenances(:summary => summary, :start_time => start_time, :end_time => (start_time + duration), :check => check_id) err = Flapjack::Diner.error unless err.nil? Flapjack.logger.info "Could not create scheduled maintenance: #{err}" session[:error] = "Could not create scheduled maintenance for the check." end redirect back end patch '/checks/:id' do check_id = params[:id] Flapjack::Diner.update_checks(:id => check_id, :enabled => false) err = Flapjack::Diner.error unless err.nil? Flapjack.logger.info "Could not disable check: #{err}" session[:error] = "Could not disable the check." end redirect '/' end patch '/scheduled_maintenances/:id' do scheduled_maintenance_id = params[:id] Flapjack::Diner.update_scheduled_maintenances({:id => scheduled_maintenance_id, :end_time => Time.now}) err = Flapjack::Diner.error unless err.nil? Flapjack.logger.info "Could not end scheduled maintenance: #{err}" session[:error] = "Could not end scheduled maintenance." end redirect back end # FIXME should fail if its start time or end_time is in the past # we'll allow the API to delete without fear or favour though delete '/scheduled_maintenances/:id' do scheduled_maintenance_id = params[:id] Flapjack::Diner.delete_scheduled_maintenances(scheduled_maintenance_id) err = Flapjack::Diner.error unless err.nil? Flapjack.logger.info "Could not delete scheduled maintenance: #{err}" session[:error] = "Could not delete scheduled maintenance." end redirect back end get '/contacts' do opts = {} @name = params[:name] opts.update(:name => @name) unless @name.nil? @contacts = Flapjack::Diner.contacts(:page => params[:page] || 1, :filter => opts, :sort => '+name') unless @contacts.nil? @pagination = pagination_from_context(Flapjack::Diner.context) unless @pagination.nil? @links = create_pagination_links(@pagination[:page], @pagination[:total_pages]) end end erb 'contacts.html'.to_sym end get "/contacts/:id" do contact_id = params[:id] @contact = Flapjack::Diner.contacts(contact_id, :include => ['checks', 'media.alerting_checks', 'rules.tags', 'rules.media']) halt(404, "Could not find contact '#{contact_id}'") if @contact.nil? @checks = [] @media = [] @rules = [] @alerting_checks_by_media_id = {} @tags_by_rule_id = {} @media_by_rule_id = {} @checks = Flapjack::Diner.related(@contact, :checks) @media = Flapjack::Diner.related(@contact, :media) unless @media.nil? || @media.empty? @alerting_checks_by_media_id = @media.inject({}) do |memo, medium| memo[medium[:id]] = Flapjack::Diner.related(medium, :alerting_checks) memo end end @rules = Flapjack::Diner.related(@contact, :rules) unless @rules.nil? || @rules.empty? @tags_by_rule_id = @rules.inject({}) do |memo, rule| memo[rule[:id]] = Flapjack::Diner.related(rule, :tags) memo end @media_by_rule_id = @rules.inject({}) do |memo, rule| memo[rule[:id]] = Flapjack::Diner.related(rule, :media) memo end end erb 'contact.html'.to_sym end error do e = env['sinatra.error'] # trace = e.backtrace.join("\n") # puts trace # Rack::CommonLogger doesn't log requests which result in exceptions. # If you want something done properly, do it yourself... access_log = self.class.instance_variable_get('@middleware').detect {|mw| mw.first.is_a?(::Rack::CommonLogger) } unless access_log.nil? access_log.first.send(:log, status_code, ::Rack::Utils::HeaderHash.new(headers), msg, env['request_timestamp']) end self.class.instance_variable_get('@show_exceptions').pretty(env, e) end private def check_state(check, time) current_state = Flapjack::Diner.related(check, :current_state) last_changed = if current_state.nil? || current_state[:created_at].nil? nil else begin DateTime.parse(current_state[:created_at]) rescue ArgumentError Flapjack.logger.warn("error parsing check state :created_at ( #{current_state.inspect} )") end end last_updated = if current_state.nil? || current_state[:updated_at].nil? nil else begin DateTime.parse(current_state[:updated_at]) rescue ArgumentError Flapjack.logger.warn("error parsing check state :updated_at ( #{current_state.inspect} )") end end latest_notifications = Flapjack::Diner.related(check, :latest_notifications) current_scheduled_maintenances = Flapjack::Diner.related(check, :current_scheduled_maintenances) current_scheduled_maintenance = current_scheduled_maintenances.max_by do |sm| begin DateTime.parse(sm[:end_time]).to_i rescue ArgumentError Flapjack.logger.warn "Couldn't parse time from current_scheduled_maintenances" -1 end end in_scheduled_maintenance = !current_scheduled_maintenance.nil? current_unscheduled_maintenance = Flapjack::Diner.related(check, :current_unscheduled_maintenance) in_unscheduled_maintenance = !current_unscheduled_maintenance.nil? { :condition => current_state.nil? ? '-' : current_state[:condition], :summary => current_state.nil? ? '-' : current_state[:summary], :latest_notifications => (latest_notifications || []), :last_changed => last_changed, :last_updated => last_updated, :in_scheduled_maintenance => in_scheduled_maintenance, :in_unscheduled_maintenance => in_unscheduled_maintenance } end def check_extra_state(check, time) state = check_state(check, time) current_state = Flapjack::Diner.related(check, :current_state) current_scheduled_maintenances = Flapjack::Diner.related(check, :current_scheduled_maintenances) current_scheduled_maintenance = current_scheduled_maintenances.max_by do |sm| begin DateTime.parse(sm[:end_time]).to_i rescue ArgumentError Flapjack.logger.warn "Couldn't parse time from current_scheduled_maintenances" -1 end end current_unscheduled_maintenance = Flapjack::Diner.related(check, :current_unscheduled_maintenance) state.merge( :details => current_state.nil? ? '-' : current_state[:details], :perfdata => current_state.nil? ? '-' : current_state[:perfdata], :current_scheduled_maintenances => (current_scheduled_maintenances || []), :current_scheduled_maintenance => current_scheduled_maintenance, :current_unscheduled_maintenance => current_unscheduled_maintenance, ) end def pagination_from_context(context) ((context || {})[:meta] || {})[:pagination] end def require_js(*js) @required_js ||= [] @required_js += js @required_js.uniq! end def require_css(*css) @required_css ||= [] @required_css += css @required_css.uniq! end def include_required_js return "" if @required_js.nil? @required_js.map { |filename| "" }.join("\n ") end def include_required_css return "" if @required_css.nil? @required_css.map { |filename| %() }.join("\n ") end # from http://gist.github.com/98310 def link_to(url_fragment, mode=:path_only) case mode when :path_only base = @base_url when :full_url if (request.scheme == 'http' && request.port == 80 || request.scheme == 'https' && request.port == 443) port = "" else port = ":#{request.port}" end base = "#{request.scheme}://#{request.host}#{port}#{request.script_name}" else raise "Unknown script_url mode #{mode}" end "#{base}#{url_fragment}" end def page_title(string) @page_title = string end def include_page_title if instance_variable_defined?('@page_title') && !@page_title.nil? return "#{@page_title} | Flapjack" end "Flapjack" end def boolean_from_str(str) case str when '0', 'f', 'false', 'n', 'no' false when '1', 't', 'true', 'y', 'yes' true end end def create_pagination_links(page, total_pages) pages = {} pages[:first] = 1 pages[:prev] = page - 1 if (page > 1) pages[:next] = page + 1 if page < total_pages pages[:last] = total_pages url_without_params = request.url.split('?').first links = {} pages.each do |key, value| page_params = {'page' => value } new_params = request.params.merge(page_params) links[key] = "#{url_without_params}?#{new_params.to_query}" end links end end end end