litmus_paper 0.0.3

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 (52) hide show
  1. data/.gitignore +17 -0
  2. data/.rake_commit +1 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +22 -0
  7. data/README.md +31 -0
  8. data/Rakefile +6 -0
  9. data/bin/litmus +6 -0
  10. data/bin/litmusctl +7 -0
  11. data/config.ru +5 -0
  12. data/lib/facts/loadaverage.rb +6 -0
  13. data/lib/litmus_paper/app.rb +63 -0
  14. data/lib/litmus_paper/cli/admin.rb +97 -0
  15. data/lib/litmus_paper/cli/server.rb +62 -0
  16. data/lib/litmus_paper/configuration.rb +27 -0
  17. data/lib/litmus_paper/dependency/http.rb +40 -0
  18. data/lib/litmus_paper/dependency/tcp.rb +24 -0
  19. data/lib/litmus_paper/forced_health.rb +18 -0
  20. data/lib/litmus_paper/health.rb +35 -0
  21. data/lib/litmus_paper/logger.rb +15 -0
  22. data/lib/litmus_paper/metric/available_memory.rb +36 -0
  23. data/lib/litmus_paper/metric/cpu_load.rb +26 -0
  24. data/lib/litmus_paper/service.rb +51 -0
  25. data/lib/litmus_paper/status_file.rb +26 -0
  26. data/lib/litmus_paper/version.rb +3 -0
  27. data/lib/litmus_paper.rb +51 -0
  28. data/litmus_paper.gemspec +26 -0
  29. data/spec/litmus_paper/app_spec.rb +246 -0
  30. data/spec/litmus_paper/cli/admin_spec.rb +64 -0
  31. data/spec/litmus_paper/cli/server_spec.rb +16 -0
  32. data/spec/litmus_paper/configuration_spec.rb +19 -0
  33. data/spec/litmus_paper/dependency/http_spec.rb +69 -0
  34. data/spec/litmus_paper/dependency/tcp_spec.rb +35 -0
  35. data/spec/litmus_paper/health_spec.rb +71 -0
  36. data/spec/litmus_paper/metric/available_memory_spec.rb +40 -0
  37. data/spec/litmus_paper/metric/cpu_load_spec.rb +46 -0
  38. data/spec/litmus_paper/service_spec.rb +65 -0
  39. data/spec/litmus_paper/status_file_spec.rb +39 -0
  40. data/spec/litmus_paper_spec.rb +39 -0
  41. data/spec/spec_helper.rb +46 -0
  42. data/spec/support/always_available_dependency.rb +9 -0
  43. data/spec/support/config.d/passing_test.config +6 -0
  44. data/spec/support/config.d/test.config +8 -0
  45. data/spec/support/constant_metric.rb +13 -0
  46. data/spec/support/http_test_server.rb +25 -0
  47. data/spec/support/http_test_server_config.ru +3 -0
  48. data/spec/support/never_available_dependency.rb +9 -0
  49. data/spec/support/stub_facter.rb +9 -0
  50. data/spec/support/test.config +13 -0
  51. data/spec/support/test.d.config +3 -0
  52. metadata +249 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rake_commit ADDED
@@ -0,0 +1 @@
1
+ --without-prompt feature
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.8.7-p249@litmus_paper --create
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in litmus_paper.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Braintree Payment Solutions LLC
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # IpvsLitmus
2
+
3
+ Backend health tester for HA Services
4
+
5
+ [![Build Status](https://secure.travis-ci.org/braintree/litmus_paper.png)](http://travis-ci.org/braintree/litmus_paper)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'litmus_paper'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install litmus_paper
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task :default => :spec
data/bin/litmus ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'litmus_paper'
4
+ require 'litmus_paper/cli/server'
5
+
6
+ LitmusPaper::CLI::Server.new.start
data/bin/litmusctl ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'litmus_paper'
5
+ require 'litmus_paper/cli/admin'
6
+
7
+ LitmusPaper::CLI::Admin.new.run
data/config.ru ADDED
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift File.expand_path('lib', File.dirname(__FILE__))
2
+ require 'litmus_paper'
3
+
4
+ use Rack::CommonLogger, LitmusPaper::Logger.new
5
+ run LitmusPaper::App
@@ -0,0 +1,6 @@
1
+ Facter.add("loadaverage") do
2
+ setcode do
3
+ uptime = Facter::Util::Resolution.exec("uptime")
4
+ uptime.split(":").last
5
+ end
6
+ end
@@ -0,0 +1,63 @@
1
+ module LitmusPaper
2
+ class App < Sinatra::Base
3
+ get "/" do
4
+ output = "Services monitored:\n"
5
+ output += LitmusPaper.services.keys.join("\n")
6
+
7
+ text 200, output
8
+ end
9
+
10
+ post "/force/*" do
11
+ path = *status_file_path(params[:splat])
12
+ statusfile = StatusFile.new(*path)
13
+ statusfile.create(params[:reason])
14
+
15
+ text 201, "File created"
16
+ end
17
+
18
+ delete "/force/*" do
19
+ path = *status_file_path(params[:splat])
20
+ statusfile = StatusFile.new(*path)
21
+ if statusfile.exists?
22
+ statusfile.delete
23
+ text 200, "File deleted"
24
+ else
25
+ text 404, "NOT FOUND"
26
+ end
27
+ end
28
+
29
+ get "/:service/status" do
30
+ service = LitmusPaper.services[params[:service]]
31
+ if service.nil?
32
+ text 404, "NOT FOUND", { "X-Health" => "0" }
33
+ else
34
+ health = service.current_health
35
+ response_code = health.ok? ? 200 : 503
36
+ body = "Health: #{health.value}\n"
37
+ body << health.summary
38
+ text response_code, body, { "X-Health" => health.value.to_s }
39
+ end
40
+ end
41
+
42
+ get "/test/error" do
43
+ raise "an error"
44
+ end
45
+
46
+ error do
47
+ text 500, "Server Error"
48
+ end
49
+
50
+ def text(response_code, body, headers ={})
51
+ [response_code, { "Content-Type" => "text/plain" }.merge(headers), body]
52
+ end
53
+
54
+ def status_file_path(splat)
55
+ path = splat.first.split("/")
56
+ if path.size == 1
57
+ ["global_#{path.first}"]
58
+ else
59
+ path
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,97 @@
1
+ module LitmusPaper
2
+ module CLI
3
+ class Admin
4
+ def run(argv = ARGV)
5
+ command = argv.shift
6
+ send(command, argv)
7
+ end
8
+
9
+ def list(args)
10
+ options = _default_options
11
+ opt_parser = _extend_default_parser(options) do |opts|
12
+ opts.banner = "Usage: litmusctl list [options]"
13
+ end
14
+ opt_parser.parse! args
15
+
16
+ request = Net::HTTP::Get.new("/")
17
+ _litmus_request(options[:host], options[:port], request)
18
+ end
19
+
20
+ def force(args)
21
+ options = _default_options
22
+ opt_parser = _extend_default_parser(options) do |opts|
23
+ opts.banner = "Usage: litmusctl force <up|down> [service] [options]"
24
+ opts.on("-d", "--delete", "Remove status file") do
25
+ options[:delete] = true
26
+ end
27
+ opts.on("-r", "--reason=reason", String, "Reason for status file") do |reason|
28
+ options[:reason] = reason
29
+ end
30
+ end
31
+
32
+ opt_parser.parse! args
33
+ direction, service = args
34
+
35
+ if options[:delete]
36
+ request = Net::HTTP::Delete.new("/force/#{direction}/#{service}")
37
+ else
38
+ if !options.has_key?(:reason)
39
+ print "Reason? "
40
+ options[:reason] = gets.chomp
41
+ end
42
+ request = Net::HTTP::Post.new("/force/#{direction}/#{service}")
43
+ request.set_form_data('reason' => options[:reason])
44
+ end
45
+
46
+ _litmus_request(options[:host], options[:port], request)
47
+ end
48
+
49
+ def status(args)
50
+ options = _default_options
51
+ opt_parser = _extend_default_parser(options) do |opts|
52
+ opts.banner = "Usage: litmusctl status <service> [options]"
53
+ end
54
+
55
+ opt_parser.parse! args
56
+ service = args.shift
57
+
58
+ _litmus_request(options[:host], options[:port], Net::HTTP::Get.new("/#{service}/status"))
59
+ end
60
+
61
+ def _default_options
62
+ options = { :port => 9292, :host => 'localhost' }
63
+ end
64
+
65
+ def _extend_default_parser(options, &block)
66
+ OptionParser.new do |opts|
67
+ block.call(opts)
68
+
69
+ opts.on("-p", "--port=port", Integer, "Port litmus is running on", "Default: 9292") do |port|
70
+ options[:port] = port
71
+ end
72
+ opts.on("-h", "--host=ip", String, ":Host litmus is running on", "Default: localhost") do |host|
73
+ options[:host] = host
74
+ end
75
+ opts.on("--help", "Show this help message.") { puts opts; exit }
76
+ end
77
+ end
78
+
79
+ def _litmus_request(host, port, request)
80
+ begin
81
+ http = Net::HTTP.start(host, port)
82
+ response = http.request(request)
83
+
84
+ puts response.body
85
+ case response
86
+ when Net::HTTPSuccess then exit 0
87
+ when Net::HTTPClientError then exit 2
88
+ else exit 1
89
+ end
90
+ rescue Errno::ECONNREFUSED => e
91
+ puts "Unable to connect to litmus on #{host}:#{port}: #{e.message}"
92
+ exit 1
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,62 @@
1
+ module LitmusPaper
2
+ module CLI
3
+ class Server < Rack::Server
4
+ class Options
5
+ def parse!(args)
6
+ args, options = args.dup, {}
7
+
8
+ opt_parser = OptionParser.new do |opts|
9
+ opts.banner = "Usage: litmus [mongrel, thin, etc] [options]"
10
+ opts.on("-c", "--config=file", String,
11
+ "Litmus configuration file", "Default: /etc/litmus.conf") { |v| options[:litmus_config] = v }
12
+ opts.on("-D", "--data-dir=path", String,
13
+ "Litmus data directory", "Default: /etc/litmus") { |v| options[:config_dir] = v }
14
+
15
+ opts.separator ""
16
+
17
+ opts.on("-p", "--port=port", Integer,
18
+ "Runs Litmus on the specified port.", "Default: 9292") { |v| options[:Port] = v }
19
+ opts.on("-b", "--binding=ip", String,
20
+ "Binds Litmus to the specified ip.", "Default: 0.0.0.0") { |v| options[:Host] = v }
21
+ opts.on("-d", "--daemon", "Make server run as a Daemon.") { options[:daemonize] = true }
22
+ opts.on("-P","--pid=pid",String,
23
+ "Specifies the PID file.",
24
+ "Default: rack.pid") { |v| options[:pid] = v }
25
+
26
+ opts.separator ""
27
+
28
+ opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
29
+ end
30
+
31
+ opt_parser.parse! args
32
+
33
+ options[:config] = File.expand_path("../../../config.ru", File.dirname(__FILE__))
34
+ options[:server] = args.shift
35
+ options
36
+ end
37
+ end
38
+
39
+ def opt_parser
40
+ Options.new
41
+ end
42
+
43
+ def start
44
+ if !File.exists?(options[:litmus_config])
45
+ puts "Could not find #{options[:litmus_config]}. Specify correct location with -c file"
46
+ exit 1
47
+ end
48
+
49
+ LitmusPaper.configure(options[:litmus_config])
50
+ LitmusPaper.config_dir = options[:config_dir]
51
+ super
52
+ end
53
+
54
+ def default_options
55
+ super.merge(
56
+ :litmus_config => '/etc/litmus.conf',
57
+ :config_dir => '/etc/litmus'
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,27 @@
1
+ module LitmusPaper
2
+ class Configuration
3
+ def initialize(config_file_path)
4
+ @config_file_path = config_file_path
5
+ @services = {}
6
+ end
7
+
8
+ def evaluate(file = @config_file_path)
9
+ config_contents = File.read(file)
10
+ instance_eval(config_contents)
11
+ @services
12
+ end
13
+
14
+ def include_files(glob_pattern)
15
+ full_glob_pattern = File.expand_path(glob_pattern, File.dirname(@config_file_path))
16
+ Dir.glob(full_glob_pattern).each do |file|
17
+ evaluate(file)
18
+ end
19
+ end
20
+
21
+ def service(name, &block)
22
+ service = Service.new(name.to_s)
23
+ block.call(service)
24
+ @services[name.to_s] = service
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ module LitmusPaper
2
+ module Dependency
3
+ class HTTP
4
+ def initialize(uri, options = {})
5
+ @uri = uri
6
+ @expected_content = Regexp.new(options.fetch(:content, '.*'))
7
+ @method = options.fetch(:method, 'GET')
8
+ end
9
+
10
+ def available?
11
+ response = _make_request
12
+ _successful_response?(response) && _body_matches?(response)
13
+ rescue Exception
14
+ false
15
+ end
16
+
17
+ def _make_request
18
+ uri = URI.parse(@uri)
19
+ request = Net::HTTP.const_get(@method.capitalize).new(uri.normalize.path)
20
+ request.set_form_data({})
21
+
22
+ Net::HTTP.start(uri.host, uri.port) do |http|
23
+ http.request(request)
24
+ end
25
+ end
26
+
27
+ def _successful_response?(response)
28
+ response.is_a? Net::HTTPSuccess
29
+ end
30
+
31
+ def _body_matches?(response)
32
+ (response.body =~ @expected_content) ? true : false
33
+ end
34
+
35
+ def to_s
36
+ "Dependency::HTTP(#{@uri})"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ module LitmusPaper
2
+ module Dependency
3
+ class TCP
4
+ def initialize(ip, port)
5
+ @ip, @port = ip, port
6
+ end
7
+
8
+ def available?
9
+ Timeout.timeout(5) do
10
+ socket = TCPSocket.new(@ip, @port)
11
+ socket.close
12
+ end
13
+ true
14
+ rescue Exception
15
+ false
16
+ end
17
+
18
+ def to_s
19
+ "Dependency::TCP(tcp://#{@ip}:#{@port})"
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,18 @@
1
+ module LitmusPaper
2
+ class ForcedHealth
3
+ attr_reader :summary
4
+
5
+ def initialize(health, summary)
6
+ @health = health
7
+ @summary = summary
8
+ end
9
+
10
+ def value
11
+ @health
12
+ end
13
+
14
+ def ok?
15
+ @health > 0
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ module LitmusPaper
2
+ class Health
3
+
4
+ attr_reader :summary
5
+
6
+ def initialize
7
+ @value = 0
8
+ @dependencies_available = true
9
+ @summary = ""
10
+ end
11
+
12
+ def ok?
13
+ value > 0
14
+ end
15
+
16
+ def value
17
+ return 0 unless @dependencies_available
18
+ @value
19
+ end
20
+
21
+ def perform(metric)
22
+ health = metric.current_health
23
+
24
+ @value += health
25
+ @summary << "#{metric}: #{health}\n"
26
+ end
27
+
28
+ def ensure(dependency)
29
+ available = dependency.available?
30
+
31
+ @dependencies_available &&= available
32
+ @summary << "#{dependency}: #{available ? 'OK' : 'FAIL'}\n"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ module LitmusPaper
2
+ class Logger
3
+ extend Forwardable
4
+ def_delegators :@syslog, :debug, :info
5
+
6
+ def initialize
7
+ @syslog = SyslogLogger.new("litmus_paper")
8
+ end
9
+
10
+ def write(message)
11
+ @syslog.info(message)
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ module LitmusPaper
2
+ module Metric
3
+ class AvailableMemory
4
+ MULTIPLIER = {
5
+ "GB" => 1024*1024*1024,
6
+ "MB" => 1024*1024,
7
+ "KB" => 1024
8
+ }
9
+
10
+ def initialize(weight, facter = Facter)
11
+ @weight = weight
12
+ @facter = facter
13
+ end
14
+
15
+ def current_health
16
+ @weight * memory_free / memory_total
17
+ end
18
+
19
+ def memory_total
20
+ return @memory_total unless @memory_total.nil?
21
+
22
+ size, scale = @facter.value('memorytotal').split(' ')
23
+ @memory_total = size.to_i * MULTIPLIER[scale]
24
+ end
25
+
26
+ def memory_free
27
+ size, scale = @facter.value('memoryfree').split(' ')
28
+ size.to_i * MULTIPLIER[scale]
29
+ end
30
+
31
+ def to_s
32
+ "Metric::AvailableMemory(#{@weight})"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ module LitmusPaper
2
+ module Metric
3
+ class CPULoad
4
+ def initialize(weight, facter = Facter)
5
+ @weight = weight
6
+ @facter = facter
7
+ end
8
+
9
+ def current_health
10
+ [@weight - (@weight * load_average / processor_count), 0].max
11
+ end
12
+
13
+ def processor_count
14
+ @processor_count ||= @facter.value('processorcount').to_i
15
+ end
16
+
17
+ def load_average
18
+ @facter.value('loadaverage').split(' ').first.to_f
19
+ end
20
+
21
+ def to_s
22
+ "Metric::CPULoad(#{@weight})"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ module LitmusPaper
2
+ class Service
3
+ def initialize(name, dependencies = [], checks = [])
4
+ @name = name
5
+ @dependencies = dependencies
6
+ @checks = checks
7
+ end
8
+
9
+ def success?
10
+ health > 0
11
+ end
12
+
13
+ def current_health
14
+ forced_health = _determine_forced_health
15
+ return forced_health unless forced_health.nil?
16
+
17
+ health = LitmusPaper::Health.new
18
+ @dependencies.each do |dependency|
19
+ health.ensure(dependency)
20
+ end
21
+
22
+ @checks.each do |check|
23
+ health.perform(check)
24
+ end
25
+ health
26
+ end
27
+
28
+ def measure_health(metric_class, options)
29
+ @checks << metric_class.new(options[:weight])
30
+ end
31
+
32
+ def depends(dependency_class, *args)
33
+ @dependencies << dependency_class.new(*args)
34
+ end
35
+
36
+ def _health_files
37
+ @health_files ||= [
38
+ [0, LitmusPaper::StatusFile.new('down', @name)],
39
+ [100, LitmusPaper::StatusFile.new('up', @name)],
40
+ [0, LitmusPaper::StatusFile.new('global_down')],
41
+ [100, LitmusPaper::StatusFile.new('global_up')]
42
+ ]
43
+ end
44
+
45
+ def _determine_forced_health
46
+ _health_files.map do |health, status_file|
47
+ ForcedHealth.new(health, status_file.content) if status_file.exists?
48
+ end.compact.first
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ module LitmusPaper
2
+ class StatusFile
3
+ def initialize(*filenames)
4
+ @path = File.join(LitmusPaper.config_dir, *filenames)
5
+ end
6
+
7
+ def content
8
+ File.read(@path).chomp
9
+ end
10
+
11
+ def create(reason)
12
+ FileUtils.mkdir_p(File.dirname(@path))
13
+ File.open(@path, 'w') do |file|
14
+ file.puts(reason)
15
+ end
16
+ end
17
+
18
+ def delete
19
+ FileUtils.rm(@path)
20
+ end
21
+
22
+ def exists?
23
+ File.exists?(@path)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module LitmusPaper
2
+ VERSION = "0.0.3"
3
+ end