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