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.
@@ -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
+
@@ -1,34 +1,71 @@
1
1
  module HAProxy
2
- Backend = Struct.new(:name, :servers)
3
- Frontend = Struct.new(:name, :ip, :port)
4
- Server = Struct.new(:name, :ip, :port, :options)
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
- class Config
7
- attr_accessor :options, :backends, :frontends, :option_groups
8
-
9
- def initialize(options = nil)
10
- self.options = options || {}
11
- self.backends = []
12
- self.frontends = []
13
- self.option_groups = {'global' => {}, 'defaults' => {}}
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
- def global
17
- option_groups['global']
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 defaults
21
- option_groups['defaults']
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 parse(filename)
30
- HAProxy::Parser.new.parse(filename)
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
+
@@ -1,7 +1,16 @@
1
1
  module HAProxy
2
2
  class Parser
3
- attr_accessor :verbose, :options
4
- attr_accessor :current_backend, :current_section, :current_frontend, :current_section_name
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 reset_parser_flags
16
- self.current_section = nil
17
- self.current_backend = nil
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
- # This is starting to suck. Try treetop.
22
- def parse(filename)
23
- self.reset_parser_flags
24
-
25
- config = HAProxy::Config.new
26
- start_backend(config, 'default')
27
- start_frontend(config, 'default')
28
-
29
- lines = File.readlines(filename)
30
- lines.each do |line|
31
- line.strip!
32
- case line.strip
33
- when /^(global|defaults)$/
34
- start_section(config, $1)
35
- when /^frontend\W+([^\W]+)\W+([^\W:]+):(\d+)/
36
- start_section(config, 'frontend')
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
- config
54
- end
55
-
56
- def normalize_option_string(option_string)
57
- normalized_options = option_string.strip.split
58
- normalized_options = normalized_options.first if normalized_options.size <= 1
59
- normalized_options
60
- end
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
- def append_option(name, option_string)
63
- normalized_options = normalize_option_string(option_string)
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
- puts " => Adding #{current_section_name} option : #{name} = #{normalized_options.inspect}" if verbose
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
- current_section[name] ||= []
68
- self.current_section[name] << normalized_options unless normalized_options.nil?
74
+ self.parse_result = result
75
+ config
69
76
  end
70
77
 
71
- def append_server(name, ip, port, option_string)
72
- puts " => Adding server: #{name}" if verbose
78
+ protected
73
79
 
74
- server_options = normalize_option_string(option_string)
75
- self.current_backend.servers[name] = Server.new(name, ip, port, server_options)
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 start_section(config, name)
79
- puts " => Starting option_group: #{name}" if verbose
80
-
81
- config.option_groups[name] ||= {}
82
- self.current_section_name = name
83
- self.current_section = config.option_groups[name]
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
- def start_frontend(config, name)
87
- puts " => Starting frontend: #{name}" if verbose
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
- self.current_frontend = Frontend.new(name, {})
90
- config.frontends << self.current_frontend
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 start_backend(config, name)
94
- puts " => Starting backend: #{name}" if verbose
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
- self.current_backend = Backend.new(name, {})
97
- config.backends << self.current_backend
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
- end
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
+