proxymgr 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +11 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +52 -0
  7. data/README.md +99 -0
  8. data/Rakefile +5 -0
  9. data/bin/proxymgr +74 -0
  10. data/etc/haproxy.cfg.erb +11 -0
  11. data/examples/config.yml +5 -0
  12. data/lib/proxymgr.rb +20 -0
  13. data/lib/proxymgr/callbacks.rb +17 -0
  14. data/lib/proxymgr/config.rb +130 -0
  15. data/lib/proxymgr/haproxy.rb +51 -0
  16. data/lib/proxymgr/haproxy/control.rb +46 -0
  17. data/lib/proxymgr/haproxy/process.rb +107 -0
  18. data/lib/proxymgr/haproxy/server.rb +24 -0
  19. data/lib/proxymgr/haproxy/socket.rb +67 -0
  20. data/lib/proxymgr/haproxy/socket_manager.rb +62 -0
  21. data/lib/proxymgr/haproxy/state.rb +124 -0
  22. data/lib/proxymgr/haproxy/updater.rb +74 -0
  23. data/lib/proxymgr/logging.rb +26 -0
  24. data/lib/proxymgr/platform.rb +16 -0
  25. data/lib/proxymgr/platform/linux.rb +9 -0
  26. data/lib/proxymgr/process_manager.rb +101 -0
  27. data/lib/proxymgr/process_manager/signal_handler.rb +44 -0
  28. data/lib/proxymgr/service_config.rb +12 -0
  29. data/lib/proxymgr/service_config/base.rb +16 -0
  30. data/lib/proxymgr/service_config/zookeeper.rb +33 -0
  31. data/lib/proxymgr/service_manager.rb +53 -0
  32. data/lib/proxymgr/sink.rb +100 -0
  33. data/lib/proxymgr/watcher.rb +9 -0
  34. data/lib/proxymgr/watcher/base.rb +75 -0
  35. data/lib/proxymgr/watcher/campanja_zk.rb +20 -0
  36. data/lib/proxymgr/watcher/dns.rb +36 -0
  37. data/lib/proxymgr/watcher/file.rb +45 -0
  38. data/lib/proxymgr/watcher/zookeeper.rb +61 -0
  39. data/packaging/profile.sh +1 -0
  40. data/packaging/recipe.rb +35 -0
  41. data/proxymgr.gemspec +20 -0
  42. data/spec/spec_helper.rb +23 -0
  43. data/spec/support/dummy_watcher.rb +21 -0
  44. data/spec/support/fake_proxy.rb +15 -0
  45. data/spec/support/fake_zookeeper.rb +170 -0
  46. data/spec/support/mock_servers.rb +7 -0
  47. data/spec/unit/haproxy/socket_manager_spec.rb +40 -0
  48. data/spec/unit/haproxy/updater_spec.rb +123 -0
  49. data/spec/unit/service_manager_spec.rb +49 -0
  50. data/spec/unit/sink_spec.rb +41 -0
  51. data/spec/unit/watcher/base_spec.rb +27 -0
  52. metadata +188 -0
@@ -0,0 +1,51 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ require 'proxymgr/haproxy/socket'
4
+ require 'proxymgr/haproxy/updater'
5
+ require 'proxymgr/haproxy/server'
6
+ require 'proxymgr/haproxy/control'
7
+ require 'proxymgr/haproxy/process'
8
+ require 'proxymgr/haproxy/state'
9
+ require 'proxymgr/haproxy/socket_manager'
10
+
11
+ def initialize(path, config_file, opts = {})
12
+ @path = path
13
+ @config_file = config_file
14
+
15
+ @socket_path = opts[:socket]
16
+ @global_config = opts[:global]
17
+ @defaults_config = opts[:defaults]
18
+
19
+ @socket = @socket_path ? Socket.new(@socket_path) : nil
20
+
21
+ @control = nil
22
+ end
23
+
24
+ def version
25
+ `#{@path} -v`[/version ([\d\.]+)/, 1].to_f
26
+ end
27
+
28
+ def start
29
+ @socket = @socket_path ? Socket.new(@socket_path) : nil
30
+ @control = Control.new(@path, @config_file)
31
+ opts = {:defaults => @defaults_config,
32
+ :global => @global_config,
33
+ :socket_path => @socket_path}
34
+ @socket_manager = SocketManager.new
35
+ @state = State.new(@control, @config_file, @socket_manager, @socket, opts)
36
+ @updater = Updater.new(@socket)
37
+
38
+ @state.start
39
+ end
40
+
41
+ def shutdown
42
+ @state.stop
43
+ @socket_manager.shutdown
44
+ end
45
+
46
+ def update_backends(watchers)
47
+ changeset = @updater.produce_changeset(watchers)
48
+ @state.update_state(watchers, changeset)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ class Control
4
+ include Callbacks
5
+
6
+ attr_reader :exit_code
7
+
8
+ def initialize(path, config_file)
9
+ @path = path
10
+ @config_file = config_file
11
+
12
+ @mutex = Mutex.new
13
+
14
+ callbacks :on_stop
15
+ end
16
+
17
+ def start
18
+ restart
19
+ end
20
+
21
+ def restart(fds = [])
22
+ @mutex.synchronize do
23
+ if @process
24
+ run(@process.pid, fds)
25
+ else
26
+ run(nil, fds)
27
+ end
28
+ end
29
+ end
30
+
31
+ [:wait, :stop, :exited?].each do |sym|
32
+ define_method(sym) { |*args, &blk| @process.send(sym, *args, &blk) }
33
+ end
34
+
35
+ private
36
+
37
+ def run(pid = nil, fds = [])
38
+ @process.replace if @process
39
+ @process = Process.new(@path, @config_file, fds, pid) do |status|
40
+ call(:on_stop, status)
41
+ end
42
+ @process.start
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,107 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ class Process
4
+ require 'state_machine'
5
+
6
+ include Logging
7
+
8
+ state_machine :state, :initial => :stopped do
9
+ event :start do
10
+ transition [:stopped, :exited] => :running, :if => :run
11
+ end
12
+
13
+ event :exited do
14
+ transition :running => :exited
15
+ end
16
+
17
+ event :stop do
18
+ transition :running => :shutdown
19
+ end
20
+
21
+ after_transition :running => :shutdown do |process|
22
+ process.stopping
23
+ process.process_manager.stop
24
+ end
25
+
26
+ event :replace do
27
+ transition :running => :stopping
28
+ end
29
+
30
+ event :stopping do
31
+ transition :shutdown => :stopping
32
+ end
33
+
34
+ event :stopped do
35
+ transition :stopping => :stopped
36
+ end
37
+
38
+ state :running do
39
+ def handle_stop(status)
40
+ @exit_code = status
41
+ if abnormal_exit?
42
+ exited
43
+ else
44
+ stopped
45
+ end
46
+ @callback.call status
47
+ end
48
+ end
49
+
50
+ state :stopping do
51
+ def handle_stop(status)
52
+ @exit_code = status
53
+ stopped
54
+ end
55
+ end
56
+ end
57
+
58
+ attr_reader :exit_code, :process_manager
59
+
60
+ def initialize(path, config_file, fds, old_pid = nil, &callback)
61
+ @path = path
62
+ @config_file = config_file
63
+ @old_pid = old_pid
64
+ @callback = callback
65
+ @fds = fds
66
+
67
+ super()
68
+ end
69
+
70
+ [:pid, :wait].each do |sym|
71
+ define_method(sym) { @process_manager.send(sym) }
72
+ end
73
+
74
+ private
75
+
76
+ def abnormal_exit?
77
+ @exit_code && @exit_code > 0
78
+ end
79
+
80
+ def run
81
+ args = ['-f', @config_file, '-db']
82
+ if @old_pid
83
+ args << '-sf'
84
+ args << @old_pid.to_s
85
+ end
86
+
87
+ @process_manager = ProcessManager.new(@path, args, :fds => @fds)
88
+ [:on_stdout, :on_stderr].each do |cb|
89
+ @process_manager.send(cb, &method(:parse_haproxy_log))
90
+ end
91
+ @process_manager.on_stop(&method(:handle_stop))
92
+ @process_manager.start
93
+ end
94
+
95
+ def parse_haproxy_log(line)
96
+ matches = line.scan(/^\[(.*)\] (.*)/)[0]
97
+ if matches
98
+ haproxy_level, msg = matches
99
+ level = haproxy_level == 'WARNING' ? :warn : :info
100
+ logger.send(level, msg)
101
+ else
102
+ logger.info(line)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,24 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ class Server
4
+ attr_reader :stats
5
+
6
+ def initialize(haproxy, stats)
7
+ @haproxy = haproxy
8
+ @stats = stats
9
+ end
10
+
11
+ def backend
12
+ @stats['pxname']
13
+ end
14
+
15
+ def name
16
+ @stats['svname']
17
+ end
18
+
19
+ def disabled?
20
+ @stats['status'] == 'MAINT'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,67 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ class Socket
4
+ require 'socket'
5
+
6
+ attr_reader :path
7
+
8
+ def initialize(path)
9
+ @path = path
10
+ end
11
+
12
+ def stats
13
+ headers, *rest = write('show stat')
14
+ headers = headers.gsub(/^# /, '').split(',')
15
+ rest.pop
16
+ rest.map { |d| Hash[headers.zip(d.split(','))] }
17
+ end
18
+
19
+ def enable(backend, host)
20
+ write "enable server #{backend}/#{host}"
21
+ end
22
+
23
+ def disable(backend, host)
24
+ write "disable server #{backend}/#{host}"
25
+ end
26
+
27
+ def shutdown(backend, host)
28
+ write "shutdown sessions server #{backend}/#{host}"
29
+ end
30
+
31
+ def servers
32
+ stats.each_with_object([]) do |stat, acc|
33
+ next if %w(FRONTEND BACKEND).include? stat['svname']
34
+ acc << Server.new(self, stat)
35
+ end
36
+ end
37
+
38
+ def write(cmd)
39
+ with do |socket|
40
+ socket.puts(cmd + "\n")
41
+ socket.readlines.map(&:chomp)
42
+ end
43
+ end
44
+
45
+ def connected?
46
+ begin
47
+ with do |socket|
48
+ socket.write "show info"
49
+ end
50
+ rescue Errno::ECONNREFUSED
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def with
57
+ socket = nil
58
+ begin
59
+ socket = UNIXSocket.new(@path)
60
+ yield socket
61
+ ensure
62
+ socket.close if socket
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,62 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ class SocketManager
4
+ require 'socket'
5
+ require 'fcntl'
6
+
7
+ include Logging
8
+
9
+ attr_reader :sockets
10
+
11
+ def initialize
12
+ @sockets = {}
13
+ end
14
+
15
+ def shutdown
16
+ @sockets.each do |_port, socket|
17
+ socket.close
18
+ end
19
+ end
20
+
21
+ def update(backends)
22
+ fds = backends.each_with_object({}) do |(name, backend), mapping|
23
+ socket = for_port(backend.port)
24
+ mapping[backend.port] = socket.fileno if socket
25
+ end
26
+
27
+ (@sockets.keys - fds.keys).each do |port|
28
+ @sockets.delete(port).close
29
+ end
30
+
31
+ fds
32
+ end
33
+
34
+ private
35
+
36
+ def for_port(port)
37
+ unless @sockets[port]
38
+ @sockets[port] = ::Socket.new(::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
39
+ @sockets[port].setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
40
+ retries = 0
41
+ until retries > 5
42
+ begin
43
+ @sockets[port].bind(::Socket.pack_sockaddr_in(port, '0.0.0.0'))
44
+ break
45
+ rescue Errno::EADDRINUSE
46
+ logger.info "Could not bind to #{port}: retrying..."
47
+ sleep 1
48
+ retries += 1
49
+ end
50
+ end
51
+ flags = @sockets[port].fcntl(Fcntl::F_GETFD) & ~Fcntl::FD_CLOEXEC
52
+ @sockets[port].fcntl(Fcntl::F_SETFD, flags)
53
+ end
54
+ @sockets[port]
55
+ end
56
+
57
+ def stop_port(port)
58
+ @sockets[port].close if @sockets[port]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,124 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ class State
4
+ require 'tempfile'
5
+ require 'pathname'
6
+ require 'erb'
7
+
8
+ include Logging
9
+
10
+ def initialize(process, config_file, socket_manager, socket = nil, opts = {})
11
+ @process = process
12
+ @config_file = config_file
13
+ @socket = socket
14
+ @socket_manager = socket_manager
15
+
16
+ @sleep_interval = opts[:sleep_interval] || 5
17
+ @global_config = opts[:global]
18
+ @defaults_config = opts[:defaults]
19
+ @socket_path = opts[:socket_path]
20
+
21
+ @file_descriptors = {}
22
+ @backends = {}
23
+ @config_template = ERB.new(File.read(File.join(ProxyMgr.template_dir, 'haproxy.cfg.erb')))
24
+ @mutex = Mutex.new
25
+ @cv = ConditionVariable.new
26
+ end
27
+
28
+ def start
29
+ write_config
30
+
31
+ @thread = Thread.new do
32
+ @mutex.synchronize do
33
+ sleep_interval = nil
34
+ loop do
35
+ logger.debug "Waiting..."
36
+ wait(sleep_interval)
37
+
38
+ restart_needed = true
39
+
40
+ if @changeset or @backends
41
+ if @changeset
42
+ update_state_with_changeset
43
+ restart_needed = @changeset.restart_needed?
44
+ end
45
+ @file_descriptors = @socket_manager.update(@backends)
46
+ write_config
47
+ @changeset = nil
48
+ @backends = nil
49
+ elsif @process.exited? and !sleep_interval
50
+ sleep_interval = @sleep_interval
51
+ logger.info "Haproxy exited abnormally. Sleeping for #{sleep_interval}s"
52
+ next
53
+ end
54
+
55
+ sleep_interval = nil
56
+ @process.restart(@file_descriptors.values) if restart_needed
57
+ end
58
+ end
59
+ end
60
+ @thread.abort_on_exception = true
61
+
62
+ @process.on_stop do |status|
63
+ Thread.new { signal }.join if @process.exited?
64
+ end
65
+ @process.start
66
+ end
67
+
68
+ def socket?
69
+ @socket and @socket.connected?
70
+ end
71
+
72
+ def update_state(backends, changeset)
73
+ @mutex.synchronize do
74
+ @changeset = changeset
75
+ @backends = backends
76
+ end
77
+ signal
78
+ end
79
+
80
+ def stop
81
+ @thread.kill
82
+ signal
83
+ @thread.join
84
+ begin
85
+ @process.stop
86
+ rescue Errno::ESRCH # sometimes there's no process here. that's OK.
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def update_state_with_changeset
93
+ @changeset.disable.each do |backend, hosts|
94
+ hosts.each { |host| @socket.disable backend, host }
95
+ end
96
+
97
+ @changeset.enable.each do |backend, hosts|
98
+ hosts.each { |host| @socket.enable backend, host }
99
+ end
100
+ end
101
+
102
+ def signal
103
+ @mutex.synchronize { @cv.signal }
104
+ end
105
+
106
+ def wait(timeout = nil)
107
+ @cv.wait(@mutex, timeout)
108
+ end
109
+
110
+ def write_config
111
+ f = nil
112
+ begin
113
+ f = Tempfile.new('haproxy')
114
+ f.write @config_template.result(binding)
115
+ f.close
116
+ Pathname.new(f.path).rename(@config_file)
117
+ rescue Exception => e
118
+ logger.warn "Unable to write to #{@config_file}: #{e}"
119
+ File.unlink f.path if f
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end