haproxy-cluster 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.
@@ -0,0 +1,56 @@
1
+ haproxy-cluster
2
+ ===============
3
+
4
+ > "Can we survive a rolling restart?"
5
+ >
6
+ > "How many concurrent connections right now across all load balancers?"
7
+
8
+ While there are already a handfull of [HA Proxy](http://haproxy.1wt.edu) abstraction layers on RubyGems, I wanted to be able to answer questions like those above and more, quickly, accurately, and easily. So here's one more for the pile.
9
+
10
+ `HAProxyCluster::Member` provides an ORM for HA Proxy's status page.
11
+
12
+ `HAProxyCluster` provides a simple map/reduce-inspired framework on top of `HAProxyCluster::Member`.
13
+
14
+ `haproxy_cluster` provides a shell scripting interface for `HAProxyCluster`. Exit codes are meaningful and intended to be useful from Nagios.
15
+
16
+ Do you deploy new code using a sequential restart of application servers? Using this common pattern carelessly can result in too many servers being down at the same time, and cutomers seeing errors. `haproxy_cluster` can prevent this by ensuring that every load balancer agrees that the application is up at each stage in the deployment. In the example below, we will deploy a new WAR to three Tomcat instances which are fronted by two HA Proxy instances. HA Proxy has been configured with `option httpchk /check`, a path which only returns an affirmative status code when the application is ready to serve requests.
17
+
18
+ ```bash
19
+ #!bin/bash
20
+ set -o errexit
21
+ servers="server1.example.com server2.example.com server3.example.com"
22
+ load_balancers="https://lb1.example.com:8888 http://lb2.example.com:8888"
23
+
24
+ for server in $servers ; do
25
+ haproxy_cluster --timeout=300 --eval "wait_until(true){ myapp.rolling_restartable? }" $load_balancers
26
+ scp myapp.war $server:/opt/tomcat/webapps
27
+ done
28
+ ```
29
+
30
+ The code block passed to `--eval` will not return until every load balancer reports that at least 80% of the backend servers defined for "myapp" are ready to serve requests. If this takes more than 5 minutes (300 seconds), the whole deployment is halted.
31
+
32
+ Maybe you'd like to know how many transactions per second your whole cluster is processing.
33
+
34
+ $ haproxy_cluster --eval 'poll{ puts members.map{|m|m.myapp.rate}.inject(:+) }' $load_balancers
35
+
36
+ Installation
37
+ ------------
38
+
39
+ `gem install haproxy-cluster`
40
+
41
+ Requires Ruby 1.9.2 and depends on RestClient.
42
+
43
+ Non-Features
44
+ ------------
45
+
46
+ * Doesn't try to modify configuration files. Use [haproxy-tools](https://github.com/subakva/haproxy-tools), [rhaproxy](https://github.com/jjuliano/rhaproxy), [haproxy_join](https://github.com/joewilliams/haproxy_join), or better yet, [Chef](http://www.opscode.com/chef) for that.
47
+ * Doesn't talk to sockets, yet. Use [haproxy-ruby](https://github.com/inkel/haproxy-ruby) for now if you need this. I intend to add support for this using `Net::SSH` and `socat(1)` but for now HTTP is enough for my needs.
48
+
49
+ ProTip
50
+ ------
51
+
52
+ HA Proxy's awesome creator Willy Tarrreau loves [big text files](http://haproxy.1wt.eu/download/1.5/doc/configuration.txt) and [big, flat web pages](http://haproxy.1wt.eu/). If smaller, hyperlinked documents are more your style, you should know about the two alternative documentation sources:
53
+
54
+ * http://code.google.com/p/haproxy-docs/
55
+ * http://cbonte.github.com/haproxy-dconv/configuration-1.5.html
56
+
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "haproxy_cluster/cli"
@@ -0,0 +1,95 @@
1
+ require 'haproxy_cluster/member'
2
+ require 'thread'
3
+
4
+ class HAProxyCluster
5
+ def initialize(members = [])
6
+ @members = []
7
+ threads = []
8
+ members.each do |url|
9
+ threads << Thread.new do
10
+ @members << HAProxyCluster::Member.new(url)
11
+ end
12
+ end
13
+ threads.each{|t|t.join}
14
+ end
15
+
16
+ attr_accessor :members
17
+
18
+ # Poll the cluster, executing the given block with fresh data at the
19
+ # prescribed interval.
20
+ def poll(interval = 1.0)
21
+ first = true
22
+ loop do
23
+ start = Time.now
24
+ map { poll! } unless first
25
+ first = false
26
+ yield
27
+ sleep interval - (Time.now - start)
28
+ end
29
+ end
30
+
31
+ # Poll the entire cluster using exponential backoff until the the given
32
+ # block's return value always matches the condition (expressed as boolean or
33
+ # range).
34
+ #
35
+ # A common form of this is:
36
+ #
37
+ # wait_for(true) do
38
+ # api.servers.map{|s|s.ok?}
39
+ # end
40
+ #
41
+ # This block would not return until every member of the cluster is available
42
+ # to serve requests.
43
+ #
44
+ # wait_for(1!=1){false} #=> true
45
+ # wait_for(1==1){true} #=> true
46
+ # wait_for(1..3){2} #=> true
47
+ # wait_for(true){sleep} #=> Timeout
48
+ def wait_for (condition, &code)
49
+ results = map(&code)
50
+ delay = 1.5
51
+ loop do
52
+ if reduce(condition, results.values.flatten)
53
+ return true
54
+ end
55
+ if delay > 60
56
+ puts "Too many timeouts, giving up"
57
+ return false
58
+ end
59
+ delay *= 2
60
+ sleep delay
61
+ map { poll! }
62
+ results = map(&code)
63
+ end
64
+ end
65
+
66
+ # Run the specified code against every memeber of the cluster. Results are
67
+ # returned as a Hash, with member.to_s being the key.
68
+ def map (&code)
69
+ threads = []
70
+ results = {}
71
+ @members.each do |member|
72
+ threads << Thread.new do
73
+ results[member.to_s] = member.instance_exec &code
74
+ end
75
+ end
76
+ threads.each{|t|t.join}
77
+ return results
78
+ end
79
+
80
+ # Return true or false depending on the relationship between `condition` and `values`.
81
+ # `condition` may be specified as true, false, or a Range object.
82
+ # `values` is an Array of whatever type is appropriate for the condition.
83
+ def reduce (condition, values)
84
+ case condition.class.to_s
85
+ when "Range"
86
+ values.each{ |v| return false unless condition.cover? v }
87
+ when "TrueClass", "FalseClass"
88
+ values.each{ |v| return false unless v == condition }
89
+ else
90
+ raise ArgumentError.new("Got #{condition.class.to_s} but TrueClass, FalseClass, or Range expected")
91
+ end
92
+ return true
93
+ end
94
+
95
+ end
@@ -0,0 +1,32 @@
1
+ require 'haproxy_cluster/stats_container'
2
+ require 'haproxy_cluster/server_collection'
3
+
4
+ class HAProxyCluster
5
+
6
+ class Backend < StatsContainer
7
+
8
+ def initialize
9
+ @servers = ServerCollection.new
10
+ super
11
+ end
12
+
13
+ attr_accessor :servers
14
+
15
+ def name
16
+ self.pxname
17
+ end
18
+
19
+ def rolling_restartable? (enough = 80)
20
+ up_servers = @servers.map{ |s| s.ok? }.count
21
+ if up_servers == 0
22
+ return true # All servers are down; can't hurt!
23
+ elsif Rational(up_servers,@servers.count) >= Rational(enough,100)
24
+ return true # Minumum % is satisfied
25
+ else
26
+ return false # Not enough servers are up to handle restarting #{number_to_restart} at a time.
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,61 @@
1
+ require 'rubygems'
2
+ require 'optparse'
3
+ require 'ostruct'
4
+ require 'pp'
5
+ require 'thread'
6
+ require 'timeout'
7
+ require 'haproxy_cluster/version'
8
+ require 'haproxy_cluster'
9
+
10
+ options = OpenStruct.new
11
+ OptionParser.new do |opts|
12
+ opts.banner = "Usage: #{File.basename $0} ARGS URL [URL] [...]"
13
+ opts.on("-e", "--eval=CODE", "Ruby code block to be evaluated") do |o|
14
+ options.code_string = o
15
+ end
16
+ opts.on("-v", "--verbose", "Verbose logging") do
17
+ # TODO Need better coverage here
18
+ RestClient.log = STDERR
19
+ end
20
+ opts.on("--csv", "Assume result will be an Array of Arrays and emit as CSV") do
21
+ options.csv = true
22
+ end
23
+ opts.on("-t", "--timeout=SECONDS", "Give up after TIMEOUT seconds") do |o|
24
+ options.timeout = o.to_f
25
+ end
26
+ opts.on_tail("--version", "Show version") do
27
+ puts HAProxyCluster::Version
28
+ exit
29
+ end
30
+ opts.separator "URL should be the root of an HA Proxy status page, either http:// or https://"
31
+ end.parse!
32
+ options.urls = ARGV
33
+
34
+
35
+ if options.code_string
36
+
37
+ if options.timeout
38
+ result = Timeout::timeout(options.timeout) do
39
+ Kernel.eval( options.code_string, HAProxyCluster.new(options.urls).instance_eval("binding") )
40
+ end
41
+ else
42
+ result = Kernel.eval( options.code_string, HAProxyCluster.new(options.urls).instance_eval("binding") )
43
+ end
44
+
45
+ case result.class.to_s
46
+ when "TrueClass","FalseClass"
47
+ exit result == true ? 0 : 1
48
+ when "Hash"
49
+ pp result
50
+ when "Array"
51
+ if options.csv
52
+ result.each{|row| puts row.to_csv}
53
+ else
54
+ pp result
55
+ end
56
+ else
57
+ puts result
58
+ end
59
+
60
+ end
61
+
@@ -0,0 +1,54 @@
1
+ require 'csv'
2
+ require 'rest-client'
3
+ require 'haproxy_cluster/backend'
4
+ require 'haproxy_cluster/server'
5
+
6
+ class HAProxyCluster
7
+ class Member
8
+ BACKEND = 1
9
+ SERVER = 2
10
+
11
+ def initialize(source)
12
+ @source = source
13
+ @backends = Hash.new { |h,k| h[k] = Backend.new }
14
+ if source =~ /https?:/
15
+ @type = :url
16
+ else
17
+ @type = :file
18
+ end
19
+ poll!
20
+ end
21
+
22
+ def poll!
23
+ case @type
24
+ when :url
25
+ csv = RestClient.get(@source + ';csv').gsub(/^# /,'').gsub(/,$/,'')
26
+ when :file
27
+ File.read(@source)
28
+ end
29
+ CSV.parse(csv, { :headers => :first_row, :converters => :all, :header_converters => [:downcase,:symbol] } ) do |row|
30
+ case row[:type]
31
+ when BACKEND
32
+ @backends[ row[:pxname].to_sym ].stats.merge! row.to_hash
33
+ when SERVER
34
+ @backends[ row[:pxname].to_sym ].servers << Server.new(row.to_hash, self)
35
+ end
36
+ end
37
+ end
38
+
39
+ attr_accessor :backends, :source, :type
40
+
41
+ def get_binding; binding; end
42
+ def to_s; @source; end
43
+
44
+ # Allow Backends to be accessed by dot-notation
45
+ def method_missing(m, *args, &block)
46
+ if @backends.has_key? m
47
+ @backends[m]
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,60 @@
1
+ require 'haproxy_cluster/stats_container'
2
+
3
+ class HAProxyCluster
4
+
5
+ class Server < StatsContainer
6
+
7
+ def initialize (stats,member)
8
+ @member = member
9
+ super stats
10
+ end
11
+
12
+ def name
13
+ self.svname
14
+ end
15
+
16
+ def backup?
17
+ self.bck == 1
18
+ end
19
+
20
+ def ok?
21
+ self.status == 'UP'
22
+ end
23
+
24
+ def enable!
25
+ modify! :enable
26
+ end
27
+
28
+ def disable!
29
+ modify! :disable
30
+ end
31
+
32
+ def wait_until_ok
33
+ return true if self.ok?
34
+ start = Time.now
35
+ until self.ok?
36
+ raise Timeout if Time.now > start + 10
37
+ sleep 1
38
+ @member.poll!
39
+ end
40
+ return true
41
+ end
42
+
43
+ private
44
+
45
+ def modify! (how)
46
+ case @member.type
47
+ when :url
48
+ RestClient.post @member.source, { :s => self.name, :action => how, :b => self.pxname }
49
+ @member.poll!
50
+ else
51
+ raise "Not implemented: #{how} on #{@member.type}"
52
+ end
53
+ return self.status
54
+ end
55
+
56
+ class Timeout < RuntimeError ; end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,9 @@
1
+ class HAProxyCluster
2
+ class ServerCollection < Array
3
+ def find(string)
4
+ self.select do |s|
5
+ s.name == string
6
+ end.first
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ require 'haproxy_cluster'
2
+
3
+ # Backends present summary statistics for the servers they contain, and
4
+ # individual servers also present their own specific data.
5
+ class HAProxyCluster
6
+ class StatsContainer
7
+
8
+ def initialize(stats = {})
9
+ @stats = stats
10
+ end
11
+
12
+ attr_accessor :stats
13
+
14
+ def method_missing(m, *args, &block)
15
+ if @stats.has_key? m
16
+ @stats[m]
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ class HAProxyCluster
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: haproxy-cluster
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jacob Elder
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: &70251272059180 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70251272059180
25
+ description: ! "haproxy-cluster\n===============\n\n> \"Can we survive a rolling restart?\"\n>\n>
26
+ \"How many concurrent connections right now across all load balancers?\"\n\nWhile
27
+ there are already a handfull of [HA Proxy](http://haproxy.1wt.edu) abstraction layers
28
+ on RubyGems, I wanted to be able to answer questions like those above and more,
29
+ quickly, accurately, and easily. So here's one more for the pile.\n\n`HAProxyCluster::Member`
30
+ provides an ORM for HA Proxy's status page.\n\n`HAProxyCluster` provides a simple
31
+ map/reduce-inspired framework on top of `HAProxyCluster::Member`.\n\n`haproxy_cluster`
32
+ provides a shell scripting interface for `HAProxyCluster`. Exit codes are meaningful
33
+ and intended to be useful from Nagios.\n\nDo you deploy new code using a sequential
34
+ restart of application servers? Using this common pattern carelessly can result
35
+ in too many servers being down at the same time, and cutomers seeing errors. `haproxy_cluster`
36
+ can prevent this by ensuring that every load balancer agrees that the application
37
+ is up at each stage in the deployment. In the example below, we will deploy a new
38
+ WAR to three Tomcat instances which are fronted by two HA Proxy instances. HA Proxy
39
+ has been configured with `option httpchk /check`, a path which only returns an affirmative
40
+ status code when the application is ready to serve requests.\n\n```bash\n#!bin/bash\nset
41
+ -o errexit\nservers=\"server1.example.com server2.example.com server3.example.com\"\nload_balancers=\"https://lb1.example.com:8888
42
+ http://lb2.example.com:8888\"\n\nfor server in $servers ; do\n haproxy_cluster
43
+ --timeout=300 --eval \"wait_until(true){ myapp.rolling_restartable? }\" $load_balancers\n
44
+ \ scp myapp.war $server:/opt/tomcat/webapps\ndone\n```\n\nThe code block passed
45
+ to `--eval` will not return until every load balancer reports that at least 80%
46
+ of the backend servers defined for \"myapp\" are ready to serve requests. If this
47
+ takes more than 5 minutes (300 seconds), the whole deployment is halted.\n\nMaybe
48
+ you'd like to know how many transactions per second your whole cluster is processing.\n\n
49
+ \ $ haproxy_cluster --eval 'poll{ puts members.map{|m|m.myapp.rate}.inject(:+)
50
+ }' $load_balancers\n\nInstallation\n------------\n\n`gem install haproxy-cluster`\n\nRequires
51
+ Ruby 1.9.2 and depends on RestClient.\n\nNon-Features\n------------\n\n* Doesn't
52
+ try to modify configuration files. Use [haproxy-tools](https://github.com/subakva/haproxy-tools),
53
+ [rhaproxy](https://github.com/jjuliano/rhaproxy), [haproxy_join](https://github.com/joewilliams/haproxy_join),
54
+ or better yet, [Chef](http://www.opscode.com/chef) for that.\n* Doesn't talk to
55
+ sockets, yet. Use [haproxy-ruby](https://github.com/inkel/haproxy-ruby) for now
56
+ if you need this. I intend to add support for this using `Net::SSH` and `socat(1)`
57
+ but for now HTTP is enough for my needs.\n\nProTip\n------\n\nHA Proxy's awesome
58
+ creator Willy Tarrreau loves [big text files](http://haproxy.1wt.eu/download/1.5/doc/configuration.txt)
59
+ and [big, flat web pages](http://haproxy.1wt.eu/). If smaller, hyperlinked documents
60
+ are more your style, you should know about the two alternative documentation sources:\n\n*
61
+ http://code.google.com/p/haproxy-docs/\n* http://cbonte.github.com/haproxy-dconv/configuration-1.5.html\n\n"
62
+ email:
63
+ - jacob.elder@gmail.com
64
+ executables:
65
+ - haproxy_cluster
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - README.md
70
+ - bin/haproxy_cluster
71
+ - lib/haproxy_cluster/backend.rb
72
+ - lib/haproxy_cluster/cli.rb
73
+ - lib/haproxy_cluster/member.rb
74
+ - lib/haproxy_cluster/server.rb
75
+ - lib/haproxy_cluster/server_collection.rb
76
+ - lib/haproxy_cluster/stats_container.rb
77
+ - lib/haproxy_cluster/version.rb
78
+ - lib/haproxy_cluster.rb
79
+ homepage: https://github.com/jelder/haproxy_cluster
80
+ licenses: []
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ version: 1.9.3
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 1.8.11
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Inspect and manipulate collections of HA Proxy instances
103
+ test_files: []
104
+ has_rdoc: