proxymgr 0.1

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