proxymgr 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.rubocop.yml +11 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +52 -0
- data/README.md +99 -0
- data/Rakefile +5 -0
- data/bin/proxymgr +74 -0
- data/etc/haproxy.cfg.erb +11 -0
- data/examples/config.yml +5 -0
- data/lib/proxymgr.rb +20 -0
- data/lib/proxymgr/callbacks.rb +17 -0
- data/lib/proxymgr/config.rb +130 -0
- data/lib/proxymgr/haproxy.rb +51 -0
- data/lib/proxymgr/haproxy/control.rb +46 -0
- data/lib/proxymgr/haproxy/process.rb +107 -0
- data/lib/proxymgr/haproxy/server.rb +24 -0
- data/lib/proxymgr/haproxy/socket.rb +67 -0
- data/lib/proxymgr/haproxy/socket_manager.rb +62 -0
- data/lib/proxymgr/haproxy/state.rb +124 -0
- data/lib/proxymgr/haproxy/updater.rb +74 -0
- data/lib/proxymgr/logging.rb +26 -0
- data/lib/proxymgr/platform.rb +16 -0
- data/lib/proxymgr/platform/linux.rb +9 -0
- data/lib/proxymgr/process_manager.rb +101 -0
- data/lib/proxymgr/process_manager/signal_handler.rb +44 -0
- data/lib/proxymgr/service_config.rb +12 -0
- data/lib/proxymgr/service_config/base.rb +16 -0
- data/lib/proxymgr/service_config/zookeeper.rb +33 -0
- data/lib/proxymgr/service_manager.rb +53 -0
- data/lib/proxymgr/sink.rb +100 -0
- data/lib/proxymgr/watcher.rb +9 -0
- data/lib/proxymgr/watcher/base.rb +75 -0
- data/lib/proxymgr/watcher/campanja_zk.rb +20 -0
- data/lib/proxymgr/watcher/dns.rb +36 -0
- data/lib/proxymgr/watcher/file.rb +45 -0
- data/lib/proxymgr/watcher/zookeeper.rb +61 -0
- data/packaging/profile.sh +1 -0
- data/packaging/recipe.rb +35 -0
- data/proxymgr.gemspec +20 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/dummy_watcher.rb +21 -0
- data/spec/support/fake_proxy.rb +15 -0
- data/spec/support/fake_zookeeper.rb +170 -0
- data/spec/support/mock_servers.rb +7 -0
- data/spec/unit/haproxy/socket_manager_spec.rb +40 -0
- data/spec/unit/haproxy/updater_spec.rb +123 -0
- data/spec/unit/service_manager_spec.rb +49 -0
- data/spec/unit/sink_spec.rb +41 -0
- data/spec/unit/watcher/base_spec.rb +27 -0
- 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
|