haproxy-tools 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.rdoc +13 -0
- data/Gemfile +4 -6
- data/README.rdoc +42 -1
- data/Rakefile +1 -0
- data/TODO +29 -0
- data/VERSION +1 -1
- data/docs/haproxy-1.3-configuration.txt +6668 -0
- data/docs/haproxy-1.4-configuration.txt +8566 -0
- data/haproxy-tools.gemspec +83 -0
- data/lib/haproxy/config.rb +54 -17
- data/lib/haproxy/parser.rb +119 -69
- data/lib/haproxy/renderer.rb +111 -0
- data/lib/haproxy/treetop/config.treetop +189 -0
- data/lib/haproxy/treetop/nodes.rb +174 -0
- data/lib/haproxy_tools.rb +10 -0
- data/spec/fixtures/simple.haproxy.cfg +2 -1
- data/spec/haproxy/config_spec.rb +99 -0
- data/spec/haproxy/parser_spec.rb +50 -29
- data/spec/haproxy/treetop/config_parser_spec.rb +95 -0
- metadata +62 -26
@@ -0,0 +1,83 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{haproxy-tools}
|
8
|
+
s.version = "0.2.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jason Wadsworth"]
|
12
|
+
s.date = %q{2012-04-24}
|
13
|
+
s.description = %q{Ruby tools for HAProxy, including config file management.}
|
14
|
+
s.email = %q{jdwadsworth@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc",
|
18
|
+
"TODO"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".document",
|
22
|
+
".rspec",
|
23
|
+
"CHANGES.rdoc",
|
24
|
+
"Gemfile",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.rdoc",
|
27
|
+
"Rakefile",
|
28
|
+
"TODO",
|
29
|
+
"VERSION",
|
30
|
+
"docs/haproxy-1.3-configuration.txt",
|
31
|
+
"docs/haproxy-1.4-configuration.txt",
|
32
|
+
"haproxy-tools.gemspec",
|
33
|
+
"lib/haproxy-tools.rb",
|
34
|
+
"lib/haproxy/config.rb",
|
35
|
+
"lib/haproxy/parser.rb",
|
36
|
+
"lib/haproxy/renderer.rb",
|
37
|
+
"lib/haproxy/treetop/config.treetop",
|
38
|
+
"lib/haproxy/treetop/nodes.rb",
|
39
|
+
"lib/haproxy_tools.rb",
|
40
|
+
"spec/fixtures/multi-pool.haproxy.cfg",
|
41
|
+
"spec/fixtures/simple.haproxy.cfg",
|
42
|
+
"spec/haproxy/config_spec.rb",
|
43
|
+
"spec/haproxy/parser_spec.rb",
|
44
|
+
"spec/haproxy/treetop/config_parser_spec.rb",
|
45
|
+
"spec/spec_helper.rb"
|
46
|
+
]
|
47
|
+
s.homepage = %q{http://github.com/subakva/haproxy-tools}
|
48
|
+
s.licenses = ["MIT"]
|
49
|
+
s.require_paths = ["lib"]
|
50
|
+
s.rubygems_version = %q{1.4.2}
|
51
|
+
s.summary = %q{HAProxy Tools for Ruby}
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
s.specification_version = 3
|
55
|
+
|
56
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
57
|
+
s.add_runtime_dependency(%q<net-scp>, [">= 0"])
|
58
|
+
s.add_runtime_dependency(%q<orderedhash>, [">= 0"])
|
59
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.7.0"])
|
60
|
+
s.add_development_dependency(%q<yard>, ["~> 0.7.0"])
|
61
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
62
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
63
|
+
s.add_development_dependency(%q<treetop>, [">= 0"])
|
64
|
+
else
|
65
|
+
s.add_dependency(%q<net-scp>, [">= 0"])
|
66
|
+
s.add_dependency(%q<orderedhash>, [">= 0"])
|
67
|
+
s.add_dependency(%q<rspec>, ["~> 2.7.0"])
|
68
|
+
s.add_dependency(%q<yard>, ["~> 0.7.0"])
|
69
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
70
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
71
|
+
s.add_dependency(%q<treetop>, [">= 0"])
|
72
|
+
end
|
73
|
+
else
|
74
|
+
s.add_dependency(%q<net-scp>, [">= 0"])
|
75
|
+
s.add_dependency(%q<orderedhash>, [">= 0"])
|
76
|
+
s.add_dependency(%q<rspec>, ["~> 2.7.0"])
|
77
|
+
s.add_dependency(%q<yard>, ["~> 0.7.0"])
|
78
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
79
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
80
|
+
s.add_dependency(%q<treetop>, [">= 0"])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
data/lib/haproxy/config.rb
CHANGED
@@ -1,34 +1,71 @@
|
|
1
1
|
module HAProxy
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
Default = Struct.new(:name, :options, :config)
|
3
|
+
Backend = Struct.new(:name, :options, :config, :servers)
|
4
|
+
Listener = Struct.new(:name, :host, :port, :options, :config, :servers)
|
5
|
+
Frontend = Struct.new(:name, :host, :port, :options, :config)
|
6
|
+
Server = Struct.new(:name, :host, :port, :attributes)
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
module ServerList
|
9
|
+
def add_server(name, host, options)
|
10
|
+
options ||= {}
|
11
|
+
new_server = options[:template] ? options[:template].clone : Server.new
|
12
|
+
new_server.name = name
|
13
|
+
new_server.host = host
|
14
|
+
new_server.port = options[:port] if options[:port]
|
15
|
+
new_server.attributes ||= options[:attributes] || []
|
16
|
+
self.servers[name] = new_server
|
17
|
+
new_server
|
14
18
|
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Listener
|
22
|
+
include ServerList
|
23
|
+
end
|
15
24
|
|
16
|
-
|
17
|
-
|
25
|
+
class Backend
|
26
|
+
include ServerList
|
27
|
+
end
|
28
|
+
|
29
|
+
class Config
|
30
|
+
attr_accessor :original_parse_tree, :listeners, :backends, :frontends, :global, :defaults
|
31
|
+
|
32
|
+
def initialize(parse_tree)
|
33
|
+
self.original_parse_tree = parse_tree
|
34
|
+
self.backends = []
|
35
|
+
self.listeners = []
|
36
|
+
self.frontends = []
|
37
|
+
self.defaults = []
|
38
|
+
self.global = {}
|
18
39
|
end
|
19
40
|
|
20
|
-
def
|
21
|
-
|
41
|
+
def listener(name)
|
42
|
+
self.listeners.find { |l| l.name == name }
|
22
43
|
end
|
23
44
|
|
24
45
|
def backend(name)
|
25
46
|
self.backends.find { |b| b.name == name }
|
26
47
|
end
|
27
48
|
|
49
|
+
def frontend(name)
|
50
|
+
self.frontends.find { |f| f.name == name }
|
51
|
+
end
|
52
|
+
|
53
|
+
def default(name)
|
54
|
+
self.defaults.find { |d| d.name == name }
|
55
|
+
end
|
56
|
+
|
57
|
+
def render
|
58
|
+
renderer = HAProxy::Renderer.new(self, self.original_parse_tree)
|
59
|
+
renderer.render
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
28
64
|
class << self
|
29
|
-
def
|
30
|
-
HAProxy::Parser.new.
|
65
|
+
def parse_file(filename)
|
66
|
+
HAProxy::Parser.new.parse_file(filename)
|
31
67
|
end
|
32
68
|
end
|
33
69
|
end
|
34
70
|
end
|
71
|
+
|
data/lib/haproxy/parser.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
1
|
module HAProxy
|
2
2
|
class Parser
|
3
|
-
|
4
|
-
|
3
|
+
class Error < Exception; end
|
4
|
+
|
5
|
+
# haproxy 1.3
|
6
|
+
SERVER_ATTRIBUTE_NAMES = %w{
|
7
|
+
addr backup check cookie disabled fall id inter fastinter downinter
|
8
|
+
maxconn maxqueue minconn port redir rise slowstart source track weight
|
9
|
+
}
|
10
|
+
# Added in haproxy 1.4
|
11
|
+
SERVER_ATTRIBUTE_NAMES += %w{error-limit observe on-error}
|
12
|
+
|
13
|
+
attr_accessor :verbose, :options, :parse_result
|
5
14
|
|
6
15
|
def initialize(options = nil)
|
7
16
|
options ||= {}
|
@@ -9,92 +18,133 @@ module HAProxy
|
|
9
18
|
|
10
19
|
self.options = options
|
11
20
|
self.verbose = options[:verbose]
|
12
|
-
reset_parser_flags
|
13
21
|
end
|
14
22
|
|
15
|
-
def
|
16
|
-
|
17
|
-
self.
|
18
|
-
self.current_frontend = nil
|
23
|
+
def parse_file(filename)
|
24
|
+
config_text = File.read(filename)
|
25
|
+
self.parse(config_text)
|
19
26
|
end
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
start_frontend(config, $1)
|
38
|
-
when /^backend\W+([^\W]+)/
|
39
|
-
start_section(config, 'backend')
|
40
|
-
start_backend(config, $1)
|
41
|
-
when /^server\W+([^\W]+)\W+([\d\.]+):(\d+)(.*)/
|
42
|
-
append_server($1, $2, $3, $4)
|
43
|
-
when /^([^\W]+)([^#]*)/ # match other name/value pairs; ignore comments
|
44
|
-
append_option($1, $2)
|
45
|
-
when /^$/
|
46
|
-
when /^#.*/
|
47
|
-
puts " => Ignoring comment: #{line}" if verbose
|
48
|
-
else
|
49
|
-
puts " => Skipping non-matching line: #{line}" if verbose
|
50
|
-
end
|
28
|
+
def parse(config_text)
|
29
|
+
parser = HAProxy::Treetop::ConfigParser.new
|
30
|
+
result = parser.parse(config_text)
|
31
|
+
raise HAProxy::Parser::Error.new(parser.failure_reason) if result.nil?
|
32
|
+
|
33
|
+
config = HAProxy::Config.new(result)
|
34
|
+
config.global = config_hash_from_config_section(result.global)
|
35
|
+
|
36
|
+
result.frontends.each do |fs|
|
37
|
+
f = Frontend.new
|
38
|
+
f.name = try(fs.frontend_header, :proxy_name, :content)
|
39
|
+
f.host = try(fs.frontend_header, :service_address, :host, :content)
|
40
|
+
f.port = try(fs.frontend_header, :service_address, :port, :content)
|
41
|
+
f.options = options_hash_from_config_section(fs)
|
42
|
+
f.config = config_hash_from_config_section(fs)
|
43
|
+
config.frontends << f
|
51
44
|
end
|
52
45
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
46
|
+
result.backends.each do |bs|
|
47
|
+
b = Backend.new
|
48
|
+
b.name = try(bs.backend_header, :proxy_name, :content)
|
49
|
+
b.options = options_hash_from_config_section(bs)
|
50
|
+
b.config = config_hash_from_config_section(bs)
|
51
|
+
b.servers = server_hash_from_config_section(bs)
|
52
|
+
config.backends << b
|
53
|
+
end
|
61
54
|
|
62
|
-
|
63
|
-
|
55
|
+
result.listeners.each do |ls|
|
56
|
+
l = Listener.new
|
57
|
+
l.name = try(ls.listen_header, :proxy_name, :content)
|
58
|
+
l.host = try(ls.listen_header, :service_address, :host, :content)
|
59
|
+
l.port = try(ls.listen_header, :service_address, :port, :content)
|
60
|
+
l.options = options_hash_from_config_section(ls)
|
61
|
+
l.config = config_hash_from_config_section(ls)
|
62
|
+
l.servers = server_hash_from_config_section(ls)
|
63
|
+
config.listeners << l
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
+
result.defaults.each do |ds|
|
67
|
+
d = Default.new
|
68
|
+
d.name = try(ds.defaults_header, :proxy_name, :content)
|
69
|
+
d.options = options_hash_from_config_section(ds)
|
70
|
+
d.config = config_hash_from_config_section(ds)
|
71
|
+
config.defaults << d
|
72
|
+
end
|
66
73
|
|
67
|
-
|
68
|
-
|
74
|
+
self.parse_result = result
|
75
|
+
config
|
69
76
|
end
|
70
77
|
|
71
|
-
|
72
|
-
puts " => Adding server: #{name}" if verbose
|
78
|
+
protected
|
73
79
|
|
74
|
-
|
75
|
-
|
80
|
+
def try(node, *method_names)
|
81
|
+
method_name = method_names.shift
|
82
|
+
if node.respond_to?(method_name)
|
83
|
+
next_node = node.send(method_name)
|
84
|
+
method_names.empty? ? next_node : try(next_node, *method_names)
|
85
|
+
else
|
86
|
+
nil
|
87
|
+
end
|
76
88
|
end
|
77
89
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
90
|
+
def server_hash_from_config_section(cs)
|
91
|
+
cs.servers.inject({}) do |ch, s|
|
92
|
+
value = try(s, :value, :content)
|
93
|
+
ch[s.name] = Server.new(s.name, s.host, s.port, parse_server_attributes(value))
|
94
|
+
ch
|
95
|
+
end
|
84
96
|
end
|
85
97
|
|
86
|
-
|
87
|
-
|
98
|
+
# Parses server attributes from the server value. I couldn't get manage to get treetop to do this.
|
99
|
+
#
|
100
|
+
# Types of server attributes to support:
|
101
|
+
# ipv4, boolean, string, integer, time (us, ms, s, m, h, d), url, source attributes
|
102
|
+
#
|
103
|
+
# BUG: If an attribute value matches an attribute name, the parser will assume that a new attribute value
|
104
|
+
# has started. I don't know how haproxy itself handles that situation.
|
105
|
+
def parse_server_attributes(value)
|
106
|
+
parts = value.split(/\s/)
|
107
|
+
current_name = nil
|
108
|
+
pairs = parts.inject(OrderedHash.new) do |pairs, part|
|
109
|
+
if SERVER_ATTRIBUTE_NAMES.include?(part)
|
110
|
+
current_name = part
|
111
|
+
pairs[current_name] = []
|
112
|
+
elsif current_name.nil?
|
113
|
+
raise "Invalid server attribute: #{part}"
|
114
|
+
else
|
115
|
+
pairs[current_name] << part
|
116
|
+
end
|
117
|
+
pairs
|
118
|
+
end
|
88
119
|
|
89
|
-
|
90
|
-
|
120
|
+
return clean_parsed_server_attributes(pairs)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Converts attributes with no values to true, and combines everything else into space-separated strings.
|
124
|
+
def clean_parsed_server_attributes(pairs)
|
125
|
+
pairs.each do |k,v|
|
126
|
+
if v.empty?
|
127
|
+
pairs[k] = true
|
128
|
+
else
|
129
|
+
pairs[k] = v.join(' ')
|
130
|
+
end
|
131
|
+
end
|
91
132
|
end
|
92
133
|
|
93
|
-
def
|
94
|
-
|
134
|
+
def options_hash_from_config_section(cs)
|
135
|
+
cs.option_lines.inject({}) do |ch, l|
|
136
|
+
ch[l.keyword.content] = l.value ? l.value.content : nil
|
137
|
+
ch
|
138
|
+
end
|
139
|
+
end
|
95
140
|
|
96
|
-
|
97
|
-
|
141
|
+
def config_hash_from_config_section(cs)
|
142
|
+
cs.config_lines.reject{|l| l.keyword.content == 'option'}.inject({}) do |ch, l|
|
143
|
+
ch[l.keyword.content] = l.value ? l.value.content : nil
|
144
|
+
ch
|
145
|
+
end
|
98
146
|
end
|
99
|
-
|
147
|
+
|
148
|
+
end
|
100
149
|
end
|
150
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module HAProxy
|
2
|
+
class Renderer
|
3
|
+
|
4
|
+
attr_accessor :config, :source_tree
|
5
|
+
|
6
|
+
def initialize(config, source_tree)
|
7
|
+
self.config = config
|
8
|
+
self.source_tree = source_tree
|
9
|
+
@server_list = {}
|
10
|
+
@context = self.config
|
11
|
+
@prev_context = self.config
|
12
|
+
@config_text = ''
|
13
|
+
end
|
14
|
+
|
15
|
+
def render
|
16
|
+
render_node(self.source_tree)
|
17
|
+
handle_context_change
|
18
|
+
@config_text
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_node(node)
|
22
|
+
node.elements.each do |e|
|
23
|
+
update_render_context(e)
|
24
|
+
|
25
|
+
handle_context_change if context_changed?
|
26
|
+
|
27
|
+
if e.class == HAProxy::Treetop::ServerLine
|
28
|
+
# Keep track of the servers that we've seen, so that we can detect and render new ones.
|
29
|
+
@server_list[e.name] = e
|
30
|
+
# Don't render the server element if it's been deleted from the config.
|
31
|
+
next if @context.servers[e.name].nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
if e.class == HAProxy::Treetop::ServerLine
|
35
|
+
# Use a custom rendering method for servers, since we allow them to be added/removed/changed.
|
36
|
+
render_server_element(e)
|
37
|
+
elsif e.elements && e.elements.size > 0
|
38
|
+
render_node(e)
|
39
|
+
else
|
40
|
+
@config_text << e.text_value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@config_text
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def context_changed?
|
49
|
+
@context != @prev_context
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_server_element(e)
|
53
|
+
server = @context.servers[e.name]
|
54
|
+
render_server(server)
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_server(server)
|
58
|
+
attribute_string = render_server_attributes(server.attributes)
|
59
|
+
@config_text << "\tserver #{server.name} #{server.host}:#{server.port} #{attribute_string}\n"
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_context_change
|
63
|
+
if [HAProxy::Listener, HAProxy::Backend].include?(@prev_context.class)
|
64
|
+
# Render any servers that were added
|
65
|
+
new_servers = @prev_context.servers.keys - @server_list.keys
|
66
|
+
|
67
|
+
new_servers.each do |server_name|
|
68
|
+
server = @prev_context.servers[server_name]
|
69
|
+
render_server(server)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
@server_list = {}
|
73
|
+
end
|
74
|
+
|
75
|
+
def render_server_attributes(attributes)
|
76
|
+
attribute_string = ""
|
77
|
+
attributes.each do |name, value|
|
78
|
+
attribute_string << name
|
79
|
+
attribute_string << " "
|
80
|
+
if value && value != true
|
81
|
+
attribute_string << value
|
82
|
+
attribute_string << " "
|
83
|
+
end
|
84
|
+
end
|
85
|
+
attribute_string
|
86
|
+
end
|
87
|
+
|
88
|
+
def update_render_context(e)
|
89
|
+
@prev_context = @context
|
90
|
+
case e.class.name
|
91
|
+
when 'HAProxy::Treetop::GlobalSection'
|
92
|
+
@context = @config.global
|
93
|
+
when 'HAProxy::Treetop::DefaultsSection'
|
94
|
+
section_name = e.defaults_header.proxy_name ? e.defaults_header.proxy_name.content : nil
|
95
|
+
@context = @config.default(section_name)
|
96
|
+
when 'HAProxy::Treetop::ListenSection'
|
97
|
+
section_name = e.listen_header.proxy_name ? e.listen_header.proxy_name.content : nil
|
98
|
+
@context = @config.listener(section_name)
|
99
|
+
when 'HAProxy::Treetop::FrontendSection'
|
100
|
+
section_name = e.frontend_header.proxy_name ? e.frontend_header.proxy_name.content : nil
|
101
|
+
@context = @config.frontend(section_name)
|
102
|
+
when 'HAProxy::Treetop::BackendSection'
|
103
|
+
section_name = e.backend_header.proxy_name ? e.backend_header.proxy_name.content : nil
|
104
|
+
@context = @config.backend(section_name)
|
105
|
+
else
|
106
|
+
@context = @prev_context
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|