haproxy-tools 0.1.0 → 0.2.0
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.
- 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
|
+
|