snowman-io 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -1
  3. data/README.md +3 -17
  4. data/bin/snowman +1 -0
  5. data/lib/snowman-io/api/public/README.md +36 -0
  6. data/lib/snowman-io/api/public/bootstrap/css/bootstrap-theme.css +470 -0
  7. data/lib/snowman-io/api/public/bootstrap/css/bootstrap-theme.css.map +1 -0
  8. data/lib/snowman-io/api/public/bootstrap/css/bootstrap-theme.min.css +5 -0
  9. data/lib/snowman-io/api/public/bootstrap/css/bootstrap.css +6332 -0
  10. data/lib/snowman-io/api/public/bootstrap/css/bootstrap.css.map +1 -0
  11. data/lib/snowman-io/api/public/bootstrap/css/bootstrap.min.css +5 -0
  12. data/lib/snowman-io/api/public/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  13. data/lib/snowman-io/api/public/bootstrap/fonts/glyphicons-halflings-regular.svg +229 -0
  14. data/lib/snowman-io/api/public/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  15. data/lib/snowman-io/api/public/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  16. data/lib/snowman-io/api/public/bootstrap/js/bootstrap.js +2320 -0
  17. data/lib/snowman-io/api/public/bootstrap/js/bootstrap.min.js +7 -0
  18. data/lib/snowman-io/api/public/bootstrap/js/npm.js +13 -0
  19. data/lib/snowman-io/api/public/css/normalize.css +406 -0
  20. data/lib/snowman-io/api/public/css/style.css +4 -0
  21. data/lib/snowman-io/api/public/js/app.js +13 -0
  22. data/lib/snowman-io/api/public/js/libs/ember-1.8.1.js +49740 -0
  23. data/lib/snowman-io/api/public/js/libs/handlebars-v1.3.0.js +2746 -0
  24. data/lib/snowman-io/api/public/js/libs/jquery-1.10.2.js +9789 -0
  25. data/lib/snowman-io/api/public/tests/runner.css +14 -0
  26. data/lib/snowman-io/api/public/tests/runner.js +13 -0
  27. data/lib/snowman-io/api/public/tests/tests.js +30 -0
  28. data/lib/snowman-io/api/public/tests/vendor/qunit-1.12.0.css +244 -0
  29. data/lib/snowman-io/api/public/tests/vendor/qunit-1.12.0.js +2212 -0
  30. data/lib/snowman-io/api/views/index.erb +26 -0
  31. data/lib/snowman-io/api/views/layout.erb +24 -0
  32. data/lib/snowman-io/api/views/login.erb +21 -0
  33. data/lib/snowman-io/api/views/unpacking.erb +21 -0
  34. data/lib/snowman-io/api.rb +61 -1
  35. data/lib/snowman-io/check.rb +49 -0
  36. data/lib/snowman-io/check_result.rb +15 -0
  37. data/lib/snowman-io/checks/hosted_graphite.rb +23 -0
  38. data/lib/snowman-io/handler.rb +27 -0
  39. data/lib/snowman-io/launcher.rb +24 -0
  40. data/lib/snowman-io/notifiers/slack.rb +76 -0
  41. data/lib/snowman-io/options.rb +3 -14
  42. data/lib/snowman-io/processor.rb +32 -0
  43. data/lib/snowman-io/scheduler.rb +42 -0
  44. data/lib/snowman-io/version.rb +1 -1
  45. data/lib/snowman-io.rb +34 -5
  46. data/snowman-io.gemspec +9 -2
  47. metadata +131 -10
@@ -0,0 +1,26 @@
1
+ <% content_for :head do %>
2
+ <meta content="<%= SnowmanIO::VERSION %>" name="snowman-io-version" />
3
+ <link rel="stylesheet" href="/css/style.css">
4
+ <% end %>
5
+
6
+ <script type="text/x-handlebars">
7
+ <div class="container">
8
+ <h2>
9
+ Welcome to SnowmanIO
10
+ <small><a href="/logout">logout</a></small>
11
+ </h2>
12
+
13
+ {{outlet}}
14
+ </div>
15
+ </script>
16
+
17
+ <script type="text/x-handlebars" id="index">
18
+ <p>Version: {{model.version}}</p>
19
+ </script>
20
+
21
+ <script src="/js/libs/jquery-1.10.2.js"></script>
22
+ <script src="/js/libs/handlebars-v1.3.0.js"></script>
23
+ <script src="/js/libs/ember-1.8.1.js"></script>
24
+ <script src="/js/app.js"></script>
25
+ <!-- to activate the test runner, add the "?test" query string parameter -->
26
+ <script src="/tests/runner.js"></script>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>SnowmanIO: be a little snowy</title>
8
+
9
+ <!-- Bootstrap -->
10
+ <link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
11
+
12
+ <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
13
+ <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
14
+ <!--[if lt IE 9]>
15
+ <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
16
+ <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
17
+ <![endif]-->
18
+
19
+ <%= yield_content :head %>
20
+ </head>
21
+ <body>
22
+ <%= yield %>
23
+ </body>
24
+ </html>
@@ -0,0 +1,21 @@
1
+ <div class="container">
2
+ <div class="page-header">
3
+ <h1>SnowmanIO</h1>
4
+ </div>
5
+
6
+ <% if @wrong_password_warning %>
7
+ <div class="alert alert-danger">
8
+ Wrong password.
9
+ </div>
10
+ <% end %>
11
+
12
+ <div class="col-sm-6">
13
+ <form action="/login" method="POST" role="form">
14
+ <div class="form-group">
15
+ <label for=password>Password</label>
16
+ <input type=password name=password class="form-control">
17
+ </div>
18
+ <input type=submit value="Login" class="btn btn-default">
19
+ </form>
20
+ </div>
21
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="container">
2
+ <div class="page-header">
3
+ <h1>SnowmanIO: Installation</h1>
4
+ </div>
5
+
6
+ <% if @empty_password_warning %>
7
+ <div class="alert alert-danger">
8
+ Empty password is not allowed.
9
+ </div>
10
+ <% end %>
11
+
12
+ <div class="col-sm-6">
13
+ <form action="/unpacking" method="POST" role="form">
14
+ <div class="form-group">
15
+ <label for=password>Admin password</label>
16
+ <input type=text name=password class="form-control">
17
+ </div>
18
+ <input type=submit value="Set Admin Password" class="btn btn-primary">
19
+ </form>
20
+ </div>
21
+ </div>
@@ -1,4 +1,5 @@
1
1
  require 'sinatra'
2
+ require 'sinatra/content_for'
2
3
 
3
4
  module SnowmanIO
4
5
  class API < Sinatra::Base
@@ -8,8 +9,67 @@ module SnowmanIO
8
9
  run!(sinatra_options)
9
10
  end
10
11
 
12
+ enable :sessions
13
+ helpers Sinatra::ContentFor
14
+ set :public_folder, File.dirname(__FILE__) + "/api/public"
15
+ set :views, File.dirname(__FILE__) + "/api/views"
16
+ # TODO: fix it
17
+ set :session_secret, 'super secret'
18
+
19
+ ADMIN_PASSWORD_KEY = "admin_password_hash"
20
+
21
+ def admin_exists?
22
+ !!SnowmanIO.redis.get(ADMIN_PASSWORD_KEY)
23
+ end
24
+
25
+ def admin_authenticated?
26
+ admin_exists? && !!session[:user]
27
+ end
28
+
29
+ before do
30
+ if !admin_exists?
31
+ redirect to('/unpacking') if request.path_info != '/unpacking'
32
+ elsif !admin_authenticated?
33
+ redirect to('/login') if request.path_info != '/login'
34
+ end
35
+ end
36
+
11
37
  get "/" do
12
- "SnowManIO Home Page"
38
+ erb :index
39
+ end
40
+
41
+ get "/login" do
42
+ erb :login
43
+ end
44
+
45
+ post "/login" do
46
+ if BCrypt::Password.new(SnowmanIO.redis.get(ADMIN_PASSWORD_KEY)) == params["password"]
47
+ session[:user] = "admin"
48
+ redirect to('/')
49
+ else
50
+ @wrong_password_warning = true
51
+ erb :login
52
+ end
53
+ end
54
+
55
+ get "/logout" do
56
+ session[:user] = nil
57
+ redirect to("/login")
58
+ end
59
+
60
+ get "/unpacking" do
61
+ erb :unpacking
62
+ end
63
+
64
+ post "/unpacking" do
65
+ if params["password"].empty?
66
+ @empty_password_warning = true
67
+ erb :unpacking
68
+ else
69
+ session[:user] = "admin"
70
+ SnowmanIO.redis.set(ADMIN_PASSWORD_KEY, BCrypt::Password.create(params["password"]))
71
+ redirect to('/')
72
+ end
13
73
  end
14
74
  end
15
75
  end
@@ -0,0 +1,49 @@
1
+ require 'json'
2
+ require 'open-uri'
3
+ require "active_support/time"
4
+
5
+ require 'snowman-io/notifiers/slack'
6
+
7
+ module SnowmanIO
8
+ class Check
9
+ include Checks::HostedGraphite
10
+
11
+ DEFAULT_INTERVAL = 1.minute
12
+ class << self
13
+ def interval(value = nil)
14
+ if value
15
+ @interval = value
16
+ else
17
+ @interval || DEFAULT_INTERVAL
18
+ end
19
+ end
20
+
21
+ def human(value = nil)
22
+ if value
23
+ @human = value
24
+ else
25
+ self.name + ": #{@human}"
26
+ end
27
+ end
28
+
29
+ def notifiers
30
+ @notifiers ||= [Notifiers::Slack].select { |notifier| notifier.configured? }
31
+ end
32
+ end
33
+
34
+ def perform
35
+ if ok?
36
+ status = "success"
37
+ message = self.class.human + " - OK"
38
+ else
39
+ status = "failed"
40
+ message = self.class.human + " - FAIL"
41
+ end
42
+ CheckResult.new(self.class, status, message)
43
+ end
44
+
45
+ def ok?
46
+ raise "Implement ok? in check class"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ module SnowmanIO
2
+ class CheckResult < Struct.new(:check, :status, :message)
3
+ def check_name
4
+ check.name
5
+ end
6
+
7
+ def fail?
8
+ status == "failed"
9
+ end
10
+
11
+ def serialize
12
+ JSON.dump({ status: status, message: message })
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module SnowmanIO
2
+ module Checks
3
+ # Gets last value from Hosted Graphite metric (https://www.hostedgraphite.com/)
4
+ module HostedGraphite
5
+ protected
6
+
7
+ def get_hg_value(metric, options = {})
8
+ access_key = ENV["HG_KEY"]
9
+ return nil unless access_key
10
+ base_url = "https://www.hostedgraphite.com#{access_key}/graphite/render"
11
+ from = options[:from] || "-10mins"
12
+ url = base_url + "?format=json&target=#{URI.escape metric}&from=#{from}"
13
+ handle = open(url)
14
+ raw_data = JSON.parse(handle.gets)
15
+ raw = raw_data.first
16
+ datapoints = raw['datapoints'].delete_if { |v| v.first.nil? }
17
+ if datapoints.last
18
+ datapoints.last.first
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ module SnowmanIO
2
+ # Handler process result from checks, saves them
3
+ # to redis and fires notifications if needed.
4
+ class Handler
5
+ include Celluloid
6
+
7
+ def handle(result)
8
+ history_key = "history:#{result.check_name.underscore}"
9
+ SnowmanIO.redis.rpush(history_key, result.serialize)
10
+ history = SnowmanIO.redis.lrange(history_key, -4, -1).map { |result| JSON.load(result) }
11
+ previous_status_failed = if history.size < 4
12
+ false
13
+ else
14
+ ["failed", "exception"].include?(history.shift["status"])
15
+ end
16
+ if !previous_status_failed && history.size >= 3 && history.all? { |result| result["status"] == "failed" || result["status"] == "exception" }
17
+ notify_fail(result)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def notify_fail(result)
24
+ result.check.notifiers.each { |notifier| notifier.pool.async.notify(result) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ require 'snowman-io/scheduler'
2
+ require 'snowman-io/handler'
3
+
4
+ module SnowmanIO
5
+ class Launcher
6
+ include Celluloid
7
+
8
+ attr_reader :scheduler, :handler
9
+
10
+ def initialize(checks)
11
+ @handler = Handler.new_link
12
+ @scheduler = Scheduler.new_link(checks)
13
+ @scheduler.handler = handler
14
+ end
15
+
16
+ def start
17
+ scheduler.async.start
18
+ end
19
+
20
+ def stop
21
+ #TODO
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ module SnowmanIO
2
+ module Notifiers
3
+ class Slack
4
+ include Celluloid
5
+ class << self
6
+ def webhook_url
7
+ ENV["SLACK_WEBHOOK_URL"]
8
+ end
9
+
10
+ def channel
11
+ ENV["SLACK_CHANNEL"]
12
+ end
13
+
14
+ def bot_name
15
+ ENV["SLACK_BOT_NAME"]
16
+ end
17
+
18
+ def configured?
19
+ webhook_url.present?
20
+ end
21
+ end
22
+
23
+ def notify(result)
24
+ return unless self.class.configured?
25
+ post_data(result.message)
26
+ end
27
+
28
+ private
29
+
30
+ def configuration
31
+ {
32
+ :webhook_url => self.class.webhook_url,
33
+ :bot_name => self.class.bot_name || "snowman-io",
34
+ :channel => self.class.channel
35
+ }
36
+ end
37
+
38
+ def post_data(message)
39
+ uri = URI(configuration[:webhook_url])
40
+ http = Net::HTTP.new(uri.host, uri.port)
41
+ http.use_ssl = true
42
+
43
+ req = Net::HTTP::Post.new("#{uri.path}?#{uri.query}")
44
+ req.body = payload(message).to_json
45
+
46
+ response = http.request(req)
47
+ verify_response(response)
48
+ end
49
+
50
+ def slack_uri(token)
51
+ url = "https://#{team_name}.slack.com/services/hooks/incoming-webhook?token=#{token}"
52
+ URI(url)
53
+ end
54
+
55
+ def verify_response(response)
56
+ case response
57
+ when Net::HTTPSuccess
58
+ true
59
+ else
60
+ raise response.error!
61
+ end
62
+ end
63
+
64
+ # TODO: include :icon_url
65
+ def payload(text)
66
+ {
67
+ :username => configuration[:bot_name],
68
+ :attachments => [{
69
+ :text => text,
70
+ :color => "#FF0000"
71
+ }]
72
+ }.tap { |payload| payload[:channel] = channel if configuration[:channel] }
73
+ end
74
+ end
75
+ end
76
+ end
@@ -9,26 +9,15 @@ module SnowmanIO
9
9
  options = default_options
10
10
 
11
11
  opt_parser = OptionParser.new do |opts|
12
- opts.banner = "Usage: snowman COMMAND [options]"
12
+ opts.banner = "Usage: snowman [options]"
13
13
 
14
14
  opts.separator ""
15
- opts.separator "Commands"
16
- opts.separator " server Run SnowManIO server"
17
-
18
- opts.separator ""
19
- opts.separator "Server options:"
15
+ opts.separator "Options:"
20
16
  opts.on("-p", "--port PORT", "use PORT (default: 4567)") do |port|
21
17
  options[:port] = port.to_i
22
18
  end
23
19
 
24
- if args.empty?
25
- puts opts
26
- exit
27
- end
28
-
29
- options[:command] = args.shift
30
- unless AVAILABLE_COMMANDS.include?(options[:command])
31
- puts "Error: Command '#{options[:command]}' not recognized"
20
+ opts.on("-h", "--help", "show this message") do
32
21
  puts opts
33
22
  exit
34
23
  end
@@ -0,0 +1,32 @@
1
+ module SnowmanIO
2
+ # Processor initiated by Scheduler, executes check and notifies
3
+ # Scheduler about the result of the check.
4
+ class Processor
5
+ include Celluloid
6
+
7
+ def initialize(scheduler)
8
+ @scheduler = scheduler
9
+ end
10
+
11
+ # TODO: logging can be extracted in some kind of middleware
12
+ def process(check)
13
+ begin
14
+ SnowmanIO.logger.info("Processing check #{check.human}, started at #{Time.now}")
15
+ result = check.new.perform
16
+ rescue Exception => e
17
+ result = result_from_exception(check, e)
18
+ raise
19
+ ensure
20
+ SnowmanIO.logger.info("Processing check #{check.human}, finished at #{Time.now}")
21
+ @scheduler.processor_done(current_actor, result)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def result_from_exception(check, e)
28
+ message = "Check #{check.human} was interruppted by exception: #{e.class}: #{e.message}"
29
+ CheckResult.new(check, "exception", message)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ require "snowman-io/processor"
2
+
3
+ module SnowmanIO
4
+ # Scheduler schedules execution of checks.
5
+ class Scheduler
6
+ include Celluloid
7
+ trap_exit :processor_died
8
+
9
+ attr_accessor :handler
10
+
11
+ def initialize(checks)
12
+ @time = {}
13
+ checks.each do |check|
14
+ @time[check] = Time.now + check.interval
15
+ end
16
+ end
17
+
18
+ def start
19
+ every(1) { schedule_checks }
20
+ end
21
+
22
+ def processor_done(processor, result)
23
+ @handler.async.handle(result)
24
+ processor.terminate
25
+ end
26
+
27
+ private
28
+
29
+ def schedule_checks
30
+ @time.each do |check, time|
31
+ if time <= Time.now
32
+ Processor.new_link(current_actor).async.process(check)
33
+ @time[check] = Time.now + check.interval
34
+ end
35
+ end
36
+ end
37
+
38
+ def processor_died(_actor, _reason)
39
+ # TODO
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module SnowmanIO
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/snowman-io.rb CHANGED
@@ -1,17 +1,46 @@
1
+ require 'logger'
2
+ require 'redis'
3
+ require 'bcrypt'
4
+ require 'celluloid/autostart'
5
+
1
6
  require "snowman-io/version"
2
7
  require "snowman-io/api"
3
8
  require "snowman-io/options"
9
+ require "snowman-io/checks/hosted_graphite"
10
+ require "snowman-io/check"
11
+ require "snowman-io/check_result"
12
+
13
+ require "snowman-io/launcher"
4
14
 
5
15
  module SnowmanIO
6
16
  def self.start
7
17
  # parse options
8
18
  options = Options.new.parse!(ARGV)
9
19
 
10
- case options[:command]
11
- when "server"
12
- API.start(options)
13
- else
14
- abort "Unreacheable point. Please report the bug to https://github.com/snowman-io/snowman-io/issues"
20
+ Celluloid.logger = (options[:verbose] ? SnowmanIO.logger : nil)
21
+
22
+ launcher = Launcher.new(load_checks)
23
+ launcher.start
24
+
25
+ # start web server on main thread
26
+ API.start(options)
27
+ end
28
+
29
+ def self.load_checks
30
+ checks = []
31
+ Dir[Dir.pwd + "/**/*_check.rb"].each do |path|
32
+ require path
33
+ klass = path.sub(Dir.pwd + "/checks" + "/", "").sub(/\.rb$/, '').camelize
34
+ checks.push Kernel.const_get(klass)
15
35
  end
36
+ checks
37
+ end
38
+
39
+ def self.redis
40
+ @redis ||= Redis.new(url: ENV["REDIS_URL"])
41
+ end
42
+
43
+ def self.logger
44
+ @logger ||= Logger.new(STDERR)
16
45
  end
17
46
  end
data/snowman-io.gemspec CHANGED
@@ -19,8 +19,15 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.add_dependency "sinatra"
22
+ spec.add_dependency "sinatra", "~> 1.4"
23
+ spec.add_dependency "sinatra-contrib", "~> 1.4"
24
+ spec.add_dependency "celluloid", "~> 0.16.0"
25
+ spec.add_dependency "redis", "~> 3.1.0"
26
+ spec.add_dependency "activesupport", "~> 4.1.8"
27
+ spec.add_dependency "bcrypt", "~> 3.1"
28
+
23
29
  spec.add_development_dependency "bundler", "~> 1.7"
24
30
  spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "rspec"
31
+ spec.add_development_dependency "rspec", "~> 3.1"
32
+ spec.add_development_dependency "capybara", "~> 2.4"
26
33
  end