require 'sinatra/base' require 'sinatra/reloader' require 'sequel' require 'json' require 'uri' module Perus::Server class App < Sinatra::Application #---------------------- # config #---------------------- helpers Helpers configure do set :root, File.join(__dir__) end configure :development do register Sinatra::Reloader end before do load_site_information end #---------------------- # admin pages #---------------------- extend Admin admin :system admin :group admin :alert admin :config, true admin :script, true # static admin index page get '/admin' do redirect "#{url_prefix}admin/systems" end post '/admin/scripts/:id/commands' do script = Script.with_pk!(params['id']) script_command = ScriptCommand.new script_command.script_id = params['id'] script_command.order = script.largest_order + 1 command_config = CommandConfig.create_with_params(params) script_command.command_config_id = command_config.id begin script_command.save rescue if script_command.command_config_id CommandConfig.with_pk!(script_command.command_config_id).destroy end end redirect "#{url_prefix}admin/scripts/#{params['id']}" end post '/admin/scripts/:script_id/commands/:id' do script_command = ScriptCommand.with_pk!(params['id']) if params['action'] == 'Delete' script_command.destroy elsif params['action'] == 'Update' script_command.command_config.update_options!(params) end redirect "#{url_prefix}admin/scripts/#{params['script_id']}" end post '/admin/configs/:id/metrics' do config = Config.with_pk!(params['id']) config_metric = ConfigMetric.new config_metric.config_id = params['id'] config_metric.order = config.largest_order + 1 command_config = CommandConfig.create_with_params(params) config_metric.command_config_id = command_config.id begin config_metric.save rescue if config_metric.command_config_id CommandConfig.with_pk!(config_metric.command_config_id).destroy end end redirect "#{url_prefix}admin/configs/#{params['id']}" end post '/admin/configs/:config_id/metrics/:id' do config_metric = ConfigMetric.with_pk!(params['id']) if params['action'] == 'Delete' config_metric.destroy elsif params['action'] == 'Update' config_metric.command_config.update_options!(params) end redirect "#{url_prefix}admin/configs/#{params['config_id']}" end #---------------------- # API #---------------------- # csv for graphs shown on system page get '/systems/:id/values' do system = System.with_pk!(params['id']) metrics = params[:metrics].to_s.split(',') # find all values for the requested metrics dataset = system.values_dataset.where(metric: metrics) # the graphing library requires multiple series to be provided in # row format (date, series 1, series 2 etc) so each value is # grouped into a timestamp update window updates = dataset.all.group_by(&:timestamp) # loop from start to end generating the response csv timestamps = updates.keys.sort csv = "Date,#{params['metrics']}\n" timestamps.each do |timestamp| date = Time.at(timestamp).strftime('%Y-%m-%d %H:%M:%S') csv << date << ',' values = Array.new(metrics.length) records = updates[timestamp] records.each do |record| values[metrics.index(record.metric)] = record.num_value end csv << values.join(',') << "\n" end csv end # receive data from a client post '/systems/:id/ping' do timestamp = Time.now.to_i # update the system with its last known ip and update time system = System.with_pk!(params['id']) system.last_updated = timestamp system.ip = request.ip # errors is either nil or a hash of the format - module: [err, ...] system.save_metric_errors(params, timestamp) # add each new value, a later process cleans up old values system.save_values(params, timestamp) # save action return values and prevent them from running again system.save_actions(params, timestamp) # ip, last updated, uploads and metrics are now updated. these are # stored on the system. system.save content_type :json {success: true}.to_json end # system config get '/systems/:id/config' do system = System.with_pk!(params['id']) config = { metrics: system.config.metric_hashes, actions: system.pending_actions.map(&:config_hash).flatten } content_type :json config.to_json end # render all errors in html to replace the shortened subset on the system page get '/systems/:id/errors' do system = System.with_pk!(params['id']) errors = system.collection_errors erb :errors, layout: false, locals: {errors: errors} end # clear collection errors delete '/systems/:id/errors' do system = System.with_pk!(params['id']) system.collection_errors.each(&:delete) redirect "#{url_prefix}systems/#{system.id}" end # create a new action post '/systems/:id/actions' do action = Action.new action.system_id = params['id'] if params['script_id'] action.script_id = params['script_id'] else command_config = CommandConfig.create_with_params(params) action.command_config_id = command_config.id end begin action.save rescue if action.command_config_id CommandConfig.with_pk!(action.command_config_id).destroy end end redirect "#{url_prefix}systems/#{params['id']}#actions" end # delete an action. deletion also clears any uploaded files. delete '/systems/:system_id/actions/:id' do action = Action.with_pk!(params['id']) action.destroy redirect "#{url_prefix}systems/#{params['system_id']}#actions" end #---------------------- # frontend #---------------------- # overview get '/' do systems = System.all alerts = Alert.all results = alerts.collect do |alert| begin alert.execute(systems) rescue => e "An error occurred running this alert: #{e.inspect}" end end @alerts = Hash[alerts.zip(results)] erb :index end # list of systems get '/systems' do @systems = System.all.group_by(&:orientation) @title = 'All Systems' erb :systems end # list of systems by group get '/groups/:id/systems' do group = Group.with_pk!(params['id']) @systems = group.systems.group_by(&:orientation) @title = group.name erb :systems end # info page for a system get '/systems/:id' do @system = System.with_pk!(params['id']) @uploads = @system.upload_urls metrics = @system.metrics # we're only interested in the latest value for string metrics @str_metrics = {} metrics.select(&:string?).each do |metric| value = @system.latest(metric.name) name = "#{metric.name.titlecase}:" @str_metrics[name] = value ? value.str_value : '' end # numeric values are grouped together by their first name component # and drawn on a graph. e.g cpu_all and cpu_chrome are shown together num_metrics = metrics.select(&:numeric?).map(&:name) @num_metrics = num_metrics.group_by {|n| n.split('_')[0]} # make links clickable @links = @system.links.to_s.gsub("\n", "
") URI::extract(@links).each {|uri| @links.gsub!(uri, %Q{#{uri}})} # last updated is a timestamp, conver if @system.last_updated @last_updated = Time.at(@system.last_updated).ctime else @last_updated = 'never updated' end @errors = @system.collection_errors_dataset.limit(5).all @total_error_count = @system.collection_errors_dataset.count @scripts = Script.all erb :system end # helper to make uploads publicly accessible get '/uploads/*' do path = params['splat'][0] raise 'Invalid path' if path.include?('..') full_path = File.join(Server.options.uploads_dir, path) send_file full_path end end end