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.
@@ -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
+