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