diff --git a/Gemfile b/Gemfile index 987110f5fc0ee186313422356cf769ea93c715bf..b77a7cceb3b0b8703ff0f3ea9adb2898ba3f91f5 100644 --- a/Gemfile +++ b/Gemfile @@ -21,4 +21,5 @@ platforms :jruby do gem "json-jruby", "= 1.4.6", :require => "json" gem "warbler", "= 1.2.1" gem "jruby-openssl", "= 0.7.3" + gem "rack-jetty", "= 0.2.0" end diff --git a/bin/noah b/bin/noah index 07d5346746093448d9d9b096e49fa7173ec37d7c..0e11b047aca4a2f1ead3cd36be96ca6332be4e6a 100755 --- a/bin/noah +++ b/bin/noah @@ -1,4 +1,7 @@ #!/usr/bin/env ruby -require File.join(File.dirname(__FILE__), './../noah') +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) +require 'noah/cli' -NoahApp.run! +n = Noah::CLI.new +n.parse_options +#NoahApp.run! diff --git a/lib/noah.rb b/lib/noah.rb new file mode 100644 index 0000000000000000000000000000000000000000..528d320200362c685406a9a96463557c851b8104 --- /dev/null +++ b/lib/noah.rb @@ -0,0 +1,17 @@ +require 'sinatra/base' +require 'sinatra/namespace' +require 'ohm' +require 'ohm/contrib' +begin + require 'yajl' +rescue LoadError + require 'json' +end +require 'haml' +require 'yaml' + +require 'noah/config' +require 'noah/helpers' +require 'noah/models' + +require 'noah/app' diff --git a/lib/noah/app.rb b/lib/noah/app.rb new file mode 100644 index 0000000000000000000000000000000000000000..8557651b4a5fd83dfa21f34c9a91504d2532d842 --- /dev/null +++ b/lib/noah/app.rb @@ -0,0 +1,321 @@ + +@db_settings = YAML::load File.new(File.join(File.dirname(__FILE__),'config','db.yml')).read + +class NoahApp < Sinatra::Base + register Sinatra::Namespace + helpers Sinatra::NoahHelpers + config_file = YAML::load File.new(File.join(File.dirname(__FILE__),'config','db.yml')).read + db = config_file["#{environment}"] + begin + Ohm.connect(:url => "redis://#{db["host"]}:#{db["port"]}/#{db["db"]}") + Ohm.redis.ping + rescue Errno::ECONNREFUSED => e + puts "Unable to connect to Redis. Shutting down...." + puts e.message + exit 1 + end + configure do + set :app_file, __FILE__ + set :root, File.dirname(__FILE__) + set :server, %w[thin mongrel webrick Jetty] + set :port, 9291 + set :logging, true + set :raise_errors, false + set :show_exceptions, false + log = File.new("logs/noah.log", "a") + STDOUT.reopen(log) + STDERR.reopen(log) + end + configure(:development) do + require 'sinatra/reloader' + register Sinatra::Reloader + also_reload "models.rb" + also_reload "helpers.rb" + set :port, 9292 + end + configure(:test) do + set :port, 9294 + end + + get '/' do + content_type "text/html" + + haml :index, :format => :html5 + end + + before do + content_type "application/json" + end + + not_found do + content_type "application/json" + erb :'404' + end + + error do + content_type "application/json" + erb :'500' + end + + namespace "/h" do + + get '/:hostname/:servicename/?' do |hostname, servicename| + h = host_service(hostname, servicename) + if h.nil? + halt 404 + else + h.to_json + end + end + + get '/:hostname/?' do |hostname| + h = host(:name => hostname) + if h.nil? + halt 404 + else + h.to_json + end + end + + get '/?' do + hosts.map {|h| h.to_hash} + if hosts.size == 0 + halt 404 + else + hosts.to_json + end + end + + put '/:hostname/?' do |hostname| + required_params = ["name", "status"] + data = JSON.parse(request.body.read) + (data.keys.sort == required_params.sort && data['name'] == hostname) ? (host = Host.find_or_create(:name => data['name'], :status => data['status'])) : (raise "Missing Parameters") + if host.valid? + r = {"result" => "success","id" => "#{host.id}","status" => "#{host.status}", "name" => "#{host.name}", "new_record" => host.is_new?} + r.to_json + else + raise "#{host.errors}" + end + end + + delete '/:hostname/?' do |hostname| + host = Host.find(:name => hostname).first + if host + services = [] + Service.find(:host_id => host.id).sort.each {|x| services << x; x.delete} if host.services.size > 0 + host.delete + r = {"result" => "success", "id" => "#{host.id}", "name" => "#{hostname}", "service_count" => "#{services.size}"} + r.to_json + else + halt 404 + end + end + + end + + namespace "/s" do + + get '/:servicename/:hostname/?' do |servicename, hostname| + hs = host_service(hostname, servicename) + if hs.nil? + halt 404 + else + hs.to_json + end + end + + get '/:servicename/?' do |servicename| + s = services(:name => servicename) + s.map {|x| x.to_hash} + if s.empty? + halt 404 + else + s.to_json + end + end + + get '/?' do + if services.empty? + halt 404 + else + services.map {|s| s.to_hash} + services.to_json + end + end + + put '/:servicename/?' do |servicename| + required_params = ["status", "host", "name"] + data = JSON.parse(request.body.read) + if data.keys.sort == required_params.sort + h = Host.find(:name => data['host']).first || (raise "Invalid Host") + service = Service.find_or_create(:name => servicename, :status => data['status'], :host => h) + if service.valid? + action = service.is_new? ? "create" : "update" + service.save + r = {"action" => action, "result" => "success", "id" => service.id, "host" => h.name, "name" => service.name} + r.to_json + else + raise "#{service.errors}" + end + else + raise "Missing Parameters" + end + end + + delete '/:servicename/:hostname/?' do |servicename, hostname| + host = Host.find(:name => hostname).first || (halt 404) + service = Service.find(:name => servicename, :host_id => host.id).first || (halt 404) + if host && service + service.delete + r = {"action" => "delete", "result" => "success", "id" => service.id, "host" => host.name, "service" => servicename} + r.to_json + else + halt 404 + end + end + + end + + namespace "/a" do + + get '/:appname/:config/?' do |appname, config| + app = Application.find(:name => appname).first + if app.nil? + halt 404 + else + c = Configuration.find(:name => config, :application_id => app.id).first + c.to_json + end + end + + get '/:appname/?' do |appname| + app = Application.find(:name => appname).first + if app.nil? + halt 404 + else + app.to_json + end + end + + put '/:appname/?' do |appname| + required_params = ["name"] + data = JSON.parse(request.body.read) + if data.keys.sort == required_params.sort && data['name'] == appname + app = Application.find_or_create(:name => appname) + else + raise "Missing Parameters" + end + if app.valid? + action = app.is_new? ? "create" : "update" + app.save + r = {"result" => "success","id" => app.id, "action" => action, "name" => app.name } + r.to_json + else + raise "#{app.errors}" + end + end + + delete '/:appname/?' do |appname| + app = Application.find(:name => appname).first + if app.nil? + halt 404 + else + configurations = [] + Configuration.find(:application_id => app.id).sort.each {|x| configurations << x; x.delete} if app.configurations.size > 0 + app.delete + r = {"result" => "success", "action" => "delete", "id" => "#{app.id}", "name" => "#{appname}", "configurations" => "#{configurations.size}"} + r.to_json + end + end + + get '/?' do + apps = [] + Application.all.sort.each {|a| apps << a.to_hash} + if apps.empty? + halt 404 + else + apps.to_json + end + end + + end + + namespace '/c' do + + # Need to move this out to configuration. + # Maybe bootstrap them from itself? + content_type_mapping = { + :yaml => "text/x-yaml", + :json => "application/json", + :xml => "text/xml", + :string => "text/plain" + } + + get '/:appname/:element/?' do |appname, element| + a = Application.find(:name => appname).first + if a.nil? + halt 404 + else + c = Configuration.find(:name => element, :application_id => a.id).first + content_type content_type_mapping[c.format.to_sym] if content_type_mapping[c.format.to_sym] + c.body + end + end + + get '/:appname/?' do |appname| + config = [] + a = Application.find(:name => appname).first + if a.nil? + halt 404 + else + Configuration.find(:application_id => a.id).sort.each {|c| config << c.to_hash} + config.to_json + end + end + + get '/?' do + configs = [] + Configuration.all.sort.each {|c| configs << c.to_hash} + if configs.empty? + halt 404 + else + configs.to_json + end + end + + put '/:appname/:element?' do |appname, element| + app = Application.find_or_create(:name => appname) + config = Configuration.find_or_create(:name => element, :application_id => app.id) + required_params = ["format", "body"] + data = JSON.parse(request.body.read) + data.keys.sort == required_params.sort ? (config.format = data["format"]; config.body = data["body"]) : (raise "Missing Parameters") + if config.valid? + config.save + action = config.is_new? ? "create" : "update" + dependency_action = app.is_new? ? "created" : "updated" + r = {"result" => "success","id" => "#{config.id}", "action" => action, "dependencies" => dependency_action, "application" => app.name, "item" => config.name} + r.to_json + else + raise "#{config.errors}" + end + end + + delete '/:appname/:element?' do |appname, element| + app = Application.find(:name => appname).first + if app + config = Configuration.find(:name=> element, :application_id => app.id).first + if config + config.delete + r = {"result" => "success", "id" => "#{config.id}", "action" => "delete", "application" => "#{app.name}", "item" => "#{element}"} + r.to_json + else + halt 404 + end + else + halt 404 + end + end + + end + run! if app_file == $0 +end diff --git a/lib/noah/applications.rb b/lib/noah/applications.rb new file mode 100644 index 0000000000000000000000000000000000000000..d22326e6c4f1cf1e9d170c70a2cb40baefbf7dbd --- /dev/null +++ b/lib/noah/applications.rb @@ -0,0 +1,50 @@ +module Noah + + class Application < Ohm::Model + include Ohm::Typecast + include Ohm::Timestamping + include Ohm::Callbacks + include Ohm::ExtraValidations + + attribute :name + collection :configurations, Configuration + + index :name + + def validate + assert_present :name + assert_unique :name + end + + def to_hash + arr = [] + configurations.sort.each {|c| arr << c.to_hash} + super.merge(:name => name, :updated_at => updated_at, :configurations => arr) + end + + def is_new? + self.created_at == self.updated_at + end + + class << self + def find_or_create(opts = {}) + begin + find(opts).first.nil? ? (app = create(opts)) : (app = find(opts).first) + if app.valid? + app.save + end + app + rescue Exception => e + e.message + end + end + end + end + + class Applications + def self.all(options = {}) + options.empty? ? Application.all.sort : Application.find(options).sort + end + end + +end diff --git a/lib/noah/cli.rb b/lib/noah/cli.rb new file mode 100644 index 0000000000000000000000000000000000000000..2a67871216eb2384ed6f1cf7d9dfbacd23bae2e0 --- /dev/null +++ b/lib/noah/cli.rb @@ -0,0 +1,52 @@ +require 'mixlib/cli' +module Noah + + class CLI + include Mixlib::CLI + + banner "noah (options)" + + option :config, + :short => "-c CONFIG", + :long => "--config CONFIG", + :default => "config/config.yml", + :description => "Configuration file with path" + + option :log_level, + :short => "-l LEVEL", + :long => "--log-level LEVEL", + :description => "Set the log level (debug, info, warn, error, fatal)", + :default => "info", + :proc => Proc.new { |l| l.to_sym } + + option :port, + :short => "-p PORT", + :long => "--port PORT", + :description => "Port number to listen on", + :default => "9292" + + option :rack_opts, + :short => "-r RACK_HANDLER", + :long => "--rack RACK_HANDLER", + :description => "Rack Handler to use. i.e. thin, mongrel, webbrick, Jetty (for JRuby)", + :default => "thin" + + option :redis_url, + :short => "-u REDIS_URL", + :long => "--url REDIS_URL", + :description => "Ohm-compatibile redis url. i.e. redis://localhost:6379/0", + :default => "redis://lcoalhost:6379/0", + :proc => Proc.new { |u| begin; require 'ohm'; Ohm.connect(:url => u); Ohm.redis.ping; rescue Errno::ECONNREFUSED => e; puts "Unable to connect to Redis. Not starting."; exit 1; end } + + option :help, + :short => "-h", + :long => "--help", + :description => "You're looking at it", + :on => :tail, + :boolean => true, + :show_options => true, + :exit => 0 + + end + +end diff --git a/lib/noah/config.rb b/lib/noah/config.rb index 75562dbc27cd54f3d43eaf12d142cd5172f69a69..0a90e1476299b87b1d993b03254dbe5fd45ceed8 100644 --- a/lib/noah/config.rb +++ b/lib/noah/config.rb @@ -1,23 +1,65 @@ module Noah - module Config - @conf ||= "config/config.yml" + class Config +# attr_accessor :config_file +# attr_accessor :settings +# attr_accessor :log_file +# attr_accessor :log_level +# attr_accessor :mode +# attr_accessor :database_url +# +# @conf ||= "config/config.yml" +# @logfile ||= "logs/noah.log" +# @log_level ||= "debug" +# @mode ||= "development" +# @database_url ||= "redis://localhost:6379/0" +# +# def self.configured? +# @config_file.nil? ? false : true +# end +# +# def self.configure! +# puts "Configuring Noah from: #{@config_file}" +# parse +# end +# +# private +# def parse(conf_file = @congig_file) +# begin +# @settings = YAML.load_file(conf_file) +# rescue Exception => e +# puts "Unable to parse configuration: #{conf_file}" +# puts e.message +# end +# end +# end - def self.config_file - @conf + def initialize(data = {}) + @data = {} + update!(data) end - def self.config_file=(config) - @conf = config + def update!(data) + data.each do |k,v| + self[k] = v + end end - def self.configured? - @conf.nil? ? false : true + def []=(k,v) + v.class == Hash ? @data[k.to_sym] = Config.new(v) : @data[k.to_sym] = v end - def self.configure! - puts "Configuring Noah from: #{@conf}" - end + def configured? + @data.size == 0 ? false : true + end + + def method_missing(sym, *args) + if sym.to_s =~ /(.+)=$/ + self[$1] = args.first + else + self[sym] + end + end end -end +end diff --git a/lib/noah/configurations.rb b/lib/noah/configurations.rb new file mode 100644 index 0000000000000000000000000000000000000000..dc5fd9547a4e6c526f40f18c048fbd00b90f613b --- /dev/null +++ b/lib/noah/configurations.rb @@ -0,0 +1,53 @@ +module Noah + + class Configuration < Ohm::Model + include Ohm::Typecast + include Ohm::Timestamping + include Ohm::Callbacks + include Ohm::ExtraValidations + + attribute :name + attribute :format + attribute :body + attribute :new_record + reference :application, Application + + index :name + + def validate + assert_present :name + assert_present :format + assert_present :body + assert_unique [:name, :application_id] + end + + def to_hash + super.merge(:name => name, :format => format, :body => body, :update_at => updated_at, :application => Application[application_id].name) + end + + def is_new? + self.created_at == self.updated_at + end + + class << self + def find_or_create(opts={}) + begin + if find(opts).first.nil? + conf = create(opts) + else + conf = find(opts).first + end + rescue Exception => e + e.message + end + end + end + end + + class Configurations + def self.all(options = {}) + options.empty? ? Configuration.all.sort : Configuration.find(options).sort + end + end + +end diff --git a/lib/noah/helpers.rb b/lib/noah/helpers.rb index 24819a95876741e9407dc67bd31ff6173f8cbdb5..f040e8b0f5eb106443d1cc0fd75d355d772e9fc9 100644 --- a/lib/noah/helpers.rb +++ b/lib/noah/helpers.rb @@ -1,5 +1,5 @@ module Noah - module Helpers + module SinatraHelpers def host(opts = {}) Noah::Models::Host.find(opts).first @@ -51,5 +51,5 @@ module Noah Configurations.all(opts) end end - helpers NoahHelpers + end diff --git a/lib/noah/hosts.rb b/lib/noah/hosts.rb new file mode 100644 index 0000000000000000000000000000000000000000..1e23cef885a755491ed094e1b1a4960888f68915 --- /dev/null +++ b/lib/noah/hosts.rb @@ -0,0 +1,58 @@ +module Noah + + class Host < Ohm::Model + include Ohm::Typecast + include Ohm::Timestamping + include Ohm::Callbacks + include Ohm::ExtraValidations + + attribute :name + attribute :status + collection :services, Service + + index :name + index :status + + def validate + assert_present :name + assert_present :status + assert_unique :name + assert_member :status, ["up","down","pending"] + end + + def to_hash + arr = [] + services.sort.each {|s| arr << s.to_hash} + h = {:name => name, :status => status, :created_at => created_at, :updated_at => updated_at, :services => arr} + super.merge(h) + end + + def is_new? + self.created_at == self.updated_at + end + + class << self + def find_or_create(opts = {}) + begin + # exclude requested status from lookup + h = find(opts.reject{|key,value| key == :status}).first + host = h.nil? ? create(opts) : h + host.status = opts[:status] + if host.valid? + host.save + end + host + rescue Exception => e + e.message + end + end + end + end + + class Hosts + def self.all(options = {}) + options.empty? ? Host.all.sort : Host.find(options).sort + end + end + +end diff --git a/lib/noah/models.rb b/lib/noah/models.rb index 373cd58383b193e3b37cfc6e1032a1626add2e3a..41fed5f53f99d5076b3277d8e406b599614e64d7 100644 --- a/lib/noah/models.rb +++ b/lib/noah/models.rb @@ -1,236 +1,5 @@ -require 'ohm/contrib' -module Noah - module Models - - class Host < Ohm::Model - include Ohm::Typecast - include Ohm::Timestamping - include Ohm::Callbacks - include Ohm::ExtraValidations - - attribute :name - attribute :status - collection :services, Service - - index :name - index :status - - def validate - assert_present :name - assert_present :status - assert_unique :name - assert_member :status, ["up","down","pending"] - end - - def to_hash - arr = [] - services.sort.each {|s| arr << s.to_hash} - h = {:name => name, :status => status, :created_at => created_at, :updated_at => updated_at, :services => arr} - super.merge(h) - end - - def is_new? - self.created_at == self.updated_at - end - - class << self - def find_or_create(opts = {}) - begin - # exclude requested status from lookup - h = find(opts.reject{|key,value| key == :status}).first - host = h.nil? ? create(opts) : h - host.status = opts[:status] - if host.valid? - host.save - end - host - rescue Exception => e - e.message - end - end - end - end - - class Service < Ohm::Model - include Ohm::Typecast - include Ohm::Timestamping - include Ohm::Callbacks - include Ohm::ExtraValidations - - attribute :name - attribute :status - reference :host, Host - - index :name - index :status - - def validate - assert_present :name - assert_present :status - assert_present :host_id - assert_unique [:name, :host_id] - assert_member :status, ["up", "down", "pending"] - end - - def to_hash - super.merge(:name => name, :status => status, :updated_at => updated_at, :host => Host[host_id].name) - end - - def is_new? - self.created_at == self.updated_at - end - - class << self - def find_or_create(opts = {}) - begin - # convert passed host object to host_id if passed - if opts.has_key?(:host) - opts.merge!({:host_id => opts[:host].id}) - opts.reject!{|key, value| key == :host} - end - # exclude requested status from lookup - s = find(opts.reject{|key,value| key == :status}).first - service = s.nil? ? create(opts) : s - service.status = opts[:status] - if service.valid? - service.save - end - service - rescue Exception => e - e.message - end - end - end - end - - class Configuration < Ohm::Model - include Ohm::Typecast - include Ohm::Timestamping - include Ohm::Callbacks - include Ohm::ExtraValidations - - attribute :name - attribute :format - attribute :body - attribute :new_record - reference :application, Application - - index :name - - def validate - assert_present :name - assert_present :format - assert_present :body - assert_unique [:name, :application_id] - end - - def to_hash - super.merge(:name => name, :format => format, :body => body, :update_at => updated_at, :application => Application[application_id].name) - end - - def is_new? - self.created_at == self.updated_at - end - - class << self - def find_or_create(opts={}) - begin - if find(opts).first.nil? - conf = create(opts) - else - conf = find(opts).first - end - rescue Exception => e - e.message - end - end - end - end - - class Application < Ohm::Model - include Ohm::Typecast - include Ohm::Timestamping - include Ohm::Callbacks - include Ohm::ExtraValidations - - attribute :name - collection :configurations, Configuration - - index :name - - def validate - assert_present :name - assert_unique :name - end - - def to_hash - arr = [] - configurations.sort.each {|c| arr << c.to_hash} - super.merge(:name => name, :updated_at => updated_at, :configurations => arr) - end - - def is_new? - self.created_at == self.updated_at - end - - class << self - def find_or_create(opts = {}) - begin - find(opts).first.nil? ? (app = create(opts)) : (app = find(opts).first) - if app.valid? - app.save - end - app - rescue Exception => e - e.message - end - end - end - end - - class Watcher < Ohm::Model #NYI - include Ohm::Typecast - include Ohm::Timestamping - include Ohm::Callbacks - - attribute :client - attribute :endpoint - attribute :event - attribute :action - - index :client - index :event - - def validate - assert_present :client, :endpoint, :event, :action - assert_unique [:client, :endpoint, :event, :action] - end - end - -# Some pluralized helper models - class Hosts - def self.all(options = {}) - options.empty? ? Host.all.sort : Host.find(options).sort - end - end - - class Services - def self.all(options = {}) - options.empty? ? Service.all.sort : Service.find(options).sort - end - end - - class Applications - def self.all(options = {}) - options.empty? ? Application.all.sort : Application.find(options).sort - end - end - - class Configurations - def self.all(options = {}) - options.empty? ? Configuration.all.sort : Configuration.find(options).sort - end - end - - end -end +require 'hosts' +require 'services' +require 'applications' +require 'configurations' +require 'watchers' diff --git a/lib/noah/services.rb b/lib/noah/services.rb new file mode 100644 index 0000000000000000000000000000000000000000..29c074820887856d438adedb051eaba0675e6a74 --- /dev/null +++ b/lib/noah/services.rb @@ -0,0 +1,61 @@ +module Noah + + class Service < Ohm::Model + include Ohm::Typecast + include Ohm::Timestamping + include Ohm::Callbacks + include Ohm::ExtraValidations + + attribute :name + attribute :status + reference :host, Host + + index :name + index :status + + def validate + assert_present :name + assert_present :status + assert_present :host_id + assert_unique [:name, :host_id] + assert_member :status, ["up", "down", "pending"] + end + + def to_hash + super.merge(:name => name, :status => status, :updated_at => updated_at, :host => Host[host_id].name) + end + + def is_new? + self.created_at == self.updated_at + end + + class << self + def find_or_create(opts = {}) + begin + # convert passed host object to host_id if passed + if opts.has_key?(:host) + opts.merge!({:host_id => opts[:host].id}) + opts.reject!{|key, value| key == :host} + end + # exclude requested status from lookup + s = find(opts.reject{|key,value| key == :status}).first + service = s.nil? ? create(opts) : s + service.status = opts[:status] + if service.valid? + service.save + end + service + rescue Exception => e + e.message + end + end + end + end + + class Services + def self.all(options = {}) + options.empty? ? Service.all.sort : Service.find(options).sort + end + end + +end diff --git a/lib/noah/watchers.rb b/lib/noah/watchers.rb new file mode 100644 index 0000000000000000000000000000000000000000..2c871efaaa6267e2cbf9de6bb7528c65ecd5fa43 --- /dev/null +++ b/lib/noah/watchers.rb @@ -0,0 +1,22 @@ +module Noah + + class Watcher < Ohm::Model #NYI + include Ohm::Typecast + include Ohm::Timestamping + include Ohm::Callbacks + + attribute :client + attribute :endpoint + attribute :event + attribute :action + + index :client + index :event + + def validate + assert_present :client, :endpoint, :event, :action + assert_unique [:client, :endpoint, :event, :action] + end + end + +end