snowman-io 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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