ipvs_litmus 0.0.1

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 (44) 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 +4 -0
  12. data/ipvs_litmus.gemspec +25 -0
  13. data/lib/facts/loadaverage.rb +6 -0
  14. data/lib/ipvs_litmus/app.rb +48 -0
  15. data/lib/ipvs_litmus/cli/admin.rb +86 -0
  16. data/lib/ipvs_litmus/cli/server.rb +62 -0
  17. data/lib/ipvs_litmus/configuration.rb +20 -0
  18. data/lib/ipvs_litmus/dependency/http.rb +27 -0
  19. data/lib/ipvs_litmus/forced_health.rb +18 -0
  20. data/lib/ipvs_litmus/health.rb +35 -0
  21. data/lib/ipvs_litmus/metric/available_memory.rb +32 -0
  22. data/lib/ipvs_litmus/metric/cpu_load.rb +22 -0
  23. data/lib/ipvs_litmus/service.rb +51 -0
  24. data/lib/ipvs_litmus/status_file.rb +26 -0
  25. data/lib/ipvs_litmus/version.rb +3 -0
  26. data/lib/ipvs_litmus.rb +44 -0
  27. data/spec/ipvs_litmus/app_spec.rb +215 -0
  28. data/spec/ipvs_litmus/cli/admin_spec.rb +58 -0
  29. data/spec/ipvs_litmus/cli/server_spec.rb +16 -0
  30. data/spec/ipvs_litmus/configuration_spec.rb +11 -0
  31. data/spec/ipvs_litmus/dependency/http_spec.rb +42 -0
  32. data/spec/ipvs_litmus/health_spec.rb +71 -0
  33. data/spec/ipvs_litmus/metric/available_memory_spec.rb +33 -0
  34. data/spec/ipvs_litmus/metric/cpu_load_spec.rb +39 -0
  35. data/spec/ipvs_litmus/service_spec.rb +65 -0
  36. data/spec/ipvs_litmus/status_file_spec.rb +39 -0
  37. data/spec/ipvs_litmus_spec.rb +22 -0
  38. data/spec/spec_helper.rb +21 -0
  39. data/spec/support/always_available_dependency.rb +5 -0
  40. data/spec/support/constant_metric.rb +9 -0
  41. data/spec/support/never_available_dependency.rb +5 -0
  42. data/spec/support/stub_facter.rb +9 -0
  43. data/spec/support/test.config +11 -0
  44. metadata +219 -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@ipvs_litmus --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 ipvs_litmus.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/ipvs_litmus.png)](http://travis-ci.org/braintree/ipvs_litmus)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'ipvs_litmus'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ipvs_litmus
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 'ipvs_litmus'
4
+ require 'ipvs_litmus/cli/server'
5
+
6
+ IPVSLitmus::CLI::Server.new.start
data/bin/litmusctl ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'ipvs_litmus'
5
+ require 'ipvs_litmus/cli/admin'
6
+
7
+ IPVSLitmus::CLI::Admin.new.run
data/config.ru ADDED
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('lib', File.dirname(__FILE__))
2
+ require 'ipvs_litmus'
3
+
4
+ run IPVSLitmus::App
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ipvs_litmus/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Braintreeps"]
6
+ gem.email = ["code@getbraintree.com"]
7
+ gem.description = %q{Backend health tester for HA Services}
8
+ gem.summary = %q{Backend health tester for HA Services}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "ipvs_litmus"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = IPVSLitmus::VERSION
17
+
18
+ gem.add_dependency "sinatra", "~> 1.3.2"
19
+ gem.add_dependency "facter", "~> 1.6.7"
20
+
21
+ gem.add_development_dependency "rspec", "2.9.0"
22
+ gem.add_development_dependency "rack-test", "0.6.1"
23
+ gem.add_development_dependency "rake"
24
+ gem.add_development_dependency "rake_commit", "0.13"
25
+ end
@@ -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,48 @@
1
+ module IPVSLitmus
2
+ class App < Sinatra::Base
3
+ post "/force/*" do
4
+ path = *status_file_path(params[:splat])
5
+ statusfile = StatusFile.new(*path)
6
+ statusfile.create(params[:reason])
7
+
8
+ text 201, "File created"
9
+ end
10
+
11
+ delete "/force/*" do
12
+ path = *status_file_path(params[:splat])
13
+ statusfile = StatusFile.new(*path)
14
+ if statusfile.exists?
15
+ statusfile.delete
16
+ text 200, "File deleted"
17
+ else
18
+ text 404, "NOT FOUND"
19
+ end
20
+ end
21
+
22
+ get "/:service/status" do
23
+ service = IPVSLitmus.services[params[:service]]
24
+ if service.nil?
25
+ text 404, "NOT FOUND"
26
+ else
27
+ health = service.current_health
28
+ response_code = health.ok? ? 200 : 503
29
+ body = "Health: #{health.value}\n"
30
+ body << health.summary
31
+ text response_code, body
32
+ end
33
+ end
34
+
35
+ def text(response_code, body)
36
+ [response_code, { "Content-Type" => "text/plain" }, body]
37
+ end
38
+
39
+ def status_file_path(splat)
40
+ path = splat.first.split("/")
41
+ if path.size == 1
42
+ ["global_#{path.first}"]
43
+ else
44
+ path
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,86 @@
1
+ module IPVSLitmus
2
+ module CLI
3
+ class Admin
4
+ def run(argv = ARGV)
5
+ command = argv.shift
6
+ send(command, argv)
7
+ end
8
+
9
+ def force(args)
10
+ options = _default_options
11
+ opt_parser = _extend_default_parser(options) do |opts|
12
+ opts.banner = "Usage: litmusctl force <up|down> [service] [options]"
13
+ opts.on("-d", "--delete", "Remove status file") do
14
+ options[:delete] = true
15
+ end
16
+ opts.on("-r", "--reason=reason", String, "Reason for status file") do |reason|
17
+ options[:reason] = reason
18
+ end
19
+ end
20
+
21
+ opt_parser.parse! args
22
+ direction, service = args
23
+
24
+ if options[:delete]
25
+ request = Net::HTTP::Delete.new("/force/#{direction}/#{service}")
26
+ else
27
+ if !options.has_key?(:reason)
28
+ print "Reason? "
29
+ options[:reason] = gets.chomp
30
+ end
31
+ request = Net::HTTP::Post.new("/force/#{direction}/#{service}")
32
+ request.set_form_data('reason' => options[:reason])
33
+ end
34
+
35
+ _litmus_request(options[:host], options[:port], request)
36
+ end
37
+
38
+ def status(args)
39
+ options = _default_options
40
+ opt_parser = _extend_default_parser(options) do |opts|
41
+ opts.banner = "Usage: litmusctl status <service> [options]"
42
+ end
43
+
44
+ opt_parser.parse! args
45
+ service = args.shift
46
+
47
+ _litmus_request(options[:host], options[:port], Net::HTTP::Get.new("/#{service}/status"))
48
+ end
49
+
50
+ def _default_options
51
+ options = { :port => 9292, :host => 'localhost' }
52
+ end
53
+
54
+ def _extend_default_parser(options, &block)
55
+ OptionParser.new do |opts|
56
+ block.call(opts)
57
+
58
+ opts.on("-p", "--port=port", Integer, "Port litmus is running on", "Default: 9292") do |port|
59
+ options[:port] = port
60
+ end
61
+ opts.on("-h", "--host=ip", String, ":Host litmus is running on", "Default: localhost") do |host|
62
+ options[:host] = host
63
+ end
64
+ opts.on("--help", "Show this help message.") { puts opts; exit }
65
+ end
66
+ end
67
+
68
+ def _litmus_request(host, port, request)
69
+ begin
70
+ http = Net::HTTP.start(host, port)
71
+ response = http.request(request)
72
+
73
+ puts response.body
74
+ case response
75
+ when Net::HTTPSuccess then exit 0
76
+ when Net::HTTPClientError then exit 2
77
+ else exit 1
78
+ end
79
+ rescue Errno::ECONNREFUSED => e
80
+ puts "Unable to connect to litmus on #{host}:#{port}: #{e.message}"
81
+ exit 1
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,62 @@
1
+ module IPVSLitmus
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
+ IPVSLitmus.configure(options[:litmus_config])
50
+ IPVSLitmus.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,20 @@
1
+ module IPVSLitmus
2
+ class Configuration
3
+ def initialize(config_file_path)
4
+ @config_file_path = config_file_path
5
+ end
6
+
7
+ def evaluate
8
+ config_contents = File.read(@config_file_path)
9
+ @services = {}
10
+ instance_eval(config_contents)
11
+ @services
12
+ end
13
+
14
+ def service(name, &block)
15
+ service = Service.new(name.to_s)
16
+ block.call(service)
17
+ @services[name.to_s] = service
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module IPVSLitmus
2
+ module Dependency
3
+ class HTTP
4
+ def initialize(uri, options = {})
5
+ @uri = uri
6
+ @expected_content = Regexp.new(options.fetch(:contnet, '.*'))
7
+ end
8
+
9
+ def available?
10
+ begin
11
+ response = Net::HTTP.get_response(URI.parse(@uri))
12
+ _successful_response?(response) && _body_matches?(response)
13
+ rescue Exception => e
14
+ false
15
+ end
16
+ end
17
+
18
+ def _successful_response?(response)
19
+ response.is_a? Net::HTTPSuccess
20
+ end
21
+
22
+ def _body_matches?(response)
23
+ response.body =~ @expected_content
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module IPVSLitmus
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 IPVSLitmus
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.class}: #{health}\n"
26
+ end
27
+
28
+ def ensure(dependency)
29
+ available = dependency.available?
30
+
31
+ @dependencies_available &&= available
32
+ @summary << "#{dependency.class}: #{available ? 'OK' : 'FAIL'}\n"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,32 @@
1
+ module IPVSLitmus
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
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ module IPVSLitmus
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
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ module IPVSLitmus
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 = IPVSLitmus::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, IPVSLitmus::StatusFile.new('down', @name)],
39
+ [100, IPVSLitmus::StatusFile.new('up', @name)],
40
+ [0, IPVSLitmus::StatusFile.new('global_down')],
41
+ [100, IPVSLitmus::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 IPVSLitmus
2
+ class StatusFile
3
+ def initialize(*filenames)
4
+ @path = File.join(IPVSLitmus.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 IPVSLitmus
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,44 @@
1
+ require 'pathname'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ require 'bundler/setup'
6
+ require 'sinatra/base'
7
+
8
+ require 'facter'
9
+ require 'facts/loadaverage'
10
+
11
+ require 'ipvs_litmus/app'
12
+ require 'ipvs_litmus/configuration'
13
+ require 'ipvs_litmus/dependency/http'
14
+ require 'ipvs_litmus/health'
15
+ require 'ipvs_litmus/forced_health'
16
+ require 'ipvs_litmus/metric/available_memory'
17
+ require 'ipvs_litmus/metric/cpu_load'
18
+ require 'ipvs_litmus/service'
19
+ require 'ipvs_litmus/status_file'
20
+
21
+ module IPVSLitmus
22
+ class << self
23
+ attr_reader :services, :config_dir
24
+ end
25
+
26
+ def self.configure(filename)
27
+ @config_file = filename
28
+ @services = IPVSLitmus::Configuration.new(filename).evaluate
29
+ end
30
+
31
+ def self.config_dir=(path)
32
+ @config_dir = Pathname.new(path)
33
+ end
34
+
35
+ def self.reload
36
+ configure(@config_file)
37
+ end
38
+
39
+ def self.reset
40
+ @services = {}
41
+ end
42
+ end
43
+
44
+ Signal.trap("HUP") { IPVSLitmus.reload }