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,74 @@
1
+ module ProxyMgr
2
+ class Haproxy
3
+ class Updater
4
+ include Logging
5
+
6
+ def initialize(socket)
7
+ @socket = socket
8
+ @old_watchers = {}
9
+ end
10
+
11
+ def produce_changeset(watchers)
12
+ if @socket and @socket.connected?
13
+ new_state = Hash[watchers.map do |name, watcher|
14
+ [name, watcher.servers]
15
+ end]
16
+ proxy_state = haproxy_state
17
+ restart_needed = false
18
+ (proxy_state.keys + new_state.keys).uniq.each do |name|
19
+ if @old_watchers[name] and watchers[name]
20
+ restart_needed = @old_watchers[name] != watchers[name]
21
+ else
22
+ restart_needed = true
23
+ end
24
+ break if restart_needed
25
+ end
26
+ changeset = Set.new(restart_needed, {}, {})
27
+ diff(new_state, proxy_state, changeset) unless restart_needed
28
+ @old_watchers = watchers
29
+ changeset
30
+ else
31
+ logger.debug 'No socket, not doing diffing'
32
+ Set.new(true, {}, {})
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def haproxy_state
39
+ @socket.servers.each_with_object({}) do |server, servers|
40
+ backend = servers[server.backend] ||= { :disabled => [], :enabled => [] }
41
+ if server.disabled?
42
+ backend[:disabled] << server.name
43
+ else
44
+ backend[:enabled] << server.name
45
+ end
46
+ end
47
+ end
48
+
49
+ def diff(new_state, proxy_state, changeset)
50
+ new_state.each_with_object(changeset) do |(backend, servers), cs|
51
+ if proxy_state[backend]
52
+ enabled = proxy_state[backend][:enabled]
53
+ to_disable = enabled - servers
54
+
55
+ disabled = proxy_state[backend][:disabled]
56
+ to_enable = (disabled & servers)
57
+ if ((enabled - to_disable) + to_enable).sort != servers.sort
58
+ cs.restart_needed = true
59
+ end
60
+
61
+ cs.disable[backend] = to_disable
62
+ cs.enable[backend] = to_enable
63
+ end
64
+ end
65
+ end
66
+
67
+ class Set < Struct.new(:restart_needed, :disable, :enable)
68
+ def restart_needed?
69
+ restart_needed
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,26 @@
1
+ module ProxyMgr
2
+ module Logging
3
+ require 'logger'
4
+ require 'stringio'
5
+
6
+ def logger
7
+ @logger ||= Logging.logger(self.class)
8
+ end
9
+
10
+ class << self
11
+ attr_accessor :level
12
+
13
+ def disable!
14
+ @disable = true
15
+ end
16
+
17
+ def logger(name)
18
+ sink = @disable ? StringIO.new : STDOUT
19
+ logger = Logger.new(sink)
20
+ logger.level = @level || Logger::INFO
21
+ logger.progname = name
22
+ logger
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ module ProxyMgr
2
+ module Platform
3
+ require 'proxymgr/platform/linux'
4
+
5
+ def self.method_missing(sym, *args)
6
+ case RUBY_PLATFORM
7
+ when /linux/
8
+ Platform::Linux.send(sym, *args)
9
+ else
10
+ fail UnsupportedPlatform "Your platform is not supported"
11
+ end
12
+ end
13
+
14
+ class UnsupportedPlatform < Exception; end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ module ProxyMgr
2
+ module Platform
3
+ module Linux
4
+ def self.max_fd
5
+ File.readlines('/proc/self/status').find { |x| x =~ /^FDSize:/ }.split(':').last.to_i
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,101 @@
1
+ module ProxyMgr
2
+ class ProcessManager
3
+ require 'timeout'
4
+ require 'proxymgr/process_manager/signal_handler'
5
+
6
+ include Callbacks
7
+
8
+ attr_reader :exit_code, :pid
9
+
10
+ def initialize(cmd, args = [], opts = {})
11
+ @cmd = cmd
12
+ @args = args
13
+ @pid = nil
14
+ @exit_code = nil
15
+
16
+ @timeout = opts[:timeout] || 10
17
+ @setsid = opts[:setsid] || true
18
+ @fds = opts[:fds] || []
19
+
20
+ @io_handler = nil
21
+
22
+ callbacks :on_stdout, :on_stderr, :on_stop
23
+ end
24
+
25
+ def start
26
+ stdout_read, stdout_write = IO.pipe
27
+ stderr_read, stderr_write = IO.pipe
28
+ sync_pipe = IO.pipe
29
+
30
+ @pid = Process.fork do
31
+ $stdout.reopen stdout_write
32
+ $stderr.reopen stderr_write
33
+ [stderr_read, stdout_read].each(&:close)
34
+ begin
35
+ Process.setsid if @setsid
36
+ rescue Errno::EPERM
37
+ end
38
+ sync_pipe[0].read(1)
39
+ 3.upto(Platform.max_fd).each do |fd|
40
+ begin
41
+ IO.for_fd(fd).close unless @fds.include? fd
42
+ rescue ArgumentError, Errno::EBADF
43
+ end
44
+ end
45
+ Process.exec(*([@cmd] + @args), :close_others => false)
46
+ end
47
+ self.class.register(@pid) { |status| call(:on_stop, status) }
48
+ sync_pipe[1].write(1)
49
+ ([stdout_write, stderr_write] + sync_pipe).each(&:close)
50
+
51
+ @thread = Thread.new do
52
+ stop = false
53
+ fdset = [stdout_read, stderr_read]
54
+ until stop
55
+ r = IO.select(fdset, [], fdset).first
56
+ out = {}
57
+ r.each do |pipe|
58
+ stream = pipe == stdout_read ? :stdout : :stderr
59
+ buf = out[stream] ||= ''
60
+ begin
61
+ loop { buf << pipe.read_nonblock(4096) }
62
+ rescue Errno::EWOULDBLOCK
63
+ rescue EOFError
64
+ stop = true
65
+ end
66
+ end
67
+ out.each do |stream, buf|
68
+ buf.split(/\n/).each { |line| call("on_#{stream}".to_sym, line) }
69
+ end
70
+ end
71
+ fdset.each(&:close)
72
+ end
73
+ @thread.abort_on_exception = true
74
+
75
+ @pid
76
+ end
77
+
78
+ def stop
79
+ Process.kill('TERM', @pid)
80
+ begin
81
+ Timeout.timeout(@timeout) { wait }
82
+ rescue Timeout::Error
83
+ Process.kill('KILL', @pid)
84
+ end
85
+ @thread.join if @thread
86
+ end
87
+
88
+ def wait
89
+ begin
90
+ _pid, result = Process.waitpid2(@pid)
91
+ @exit_code = result.exitstatus || result.termsig
92
+ rescue Errno::ECHILD
93
+ end
94
+ end
95
+
96
+ def self.register(pid, &blk)
97
+ @handler ||= SignalHandler.new
98
+ @handler.register(pid, &blk)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,44 @@
1
+ module ProxyMgr
2
+ class ProcessManager
3
+ class SignalHandler
4
+ include Logging
5
+
6
+ def initialize
7
+ @pids = {}
8
+
9
+ start
10
+ end
11
+
12
+ def register(pid, &blk)
13
+ @pids[pid] = blk
14
+ end
15
+
16
+ private
17
+
18
+ def start
19
+ Signal.trap(:CHLD) do
20
+ handled = {}
21
+ begin
22
+ loop do
23
+ pid, status = Process.waitpid2(-1, Process::WNOHANG)
24
+ break unless pid
25
+ handled[pid] = result(status)
26
+ end
27
+ rescue Errno::ECHILD
28
+ end
29
+ handled.each do |pid, result|
30
+ handle(pid, result)
31
+ end
32
+ end
33
+ end
34
+
35
+ def handle(pid, status = nil)
36
+ @pids.delete(pid).call(status) if @pids[pid]
37
+ end
38
+
39
+ def result(status)
40
+ status && (status.exitstatus || status.termsig)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ module ProxyMgr
2
+ module ServiceConfig
3
+ require 'proxymgr/service_config/base'
4
+ require 'proxymgr/service_config/zookeeper'
5
+
6
+ def self.create(manager, config)
7
+ type = config.delete('type')
8
+ impl = ServiceConfig.const_get(type.capitalize)
9
+ impl.new(manager, config)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module ProxyMgr
2
+ module ServiceConfig
3
+ class Base
4
+ def initialize(manager, config)
5
+ @manager = manager
6
+ @config = config
7
+
8
+ start
9
+ end
10
+
11
+ def start
12
+ fail Exception 'Should be overridden'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ module ProxyMgr
2
+ module ServiceConfig
3
+ class Zookeeper < Base
4
+ require 'yajl/json_gem'
5
+ require 'zoology'
6
+
7
+ include Logging
8
+
9
+ def start
10
+ @services = {}
11
+
12
+ @client = Zoology::Client.new(@config['servers'])
13
+ @path_cache = Zoology::PathCache.new(@client,
14
+ @config['path'],
15
+ &method(:watch))
16
+ @client.connect
17
+ end
18
+
19
+ private
20
+
21
+ def watch(path, type, req)
22
+ name = File.basename(path)
23
+ if type == :update
24
+ config = JSON.parse(req[:data])
25
+ @manager.update_service(name, config)
26
+ else
27
+ logger.debug "deleting service #{name}"
28
+ @manager.delete_service(name)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ module ProxyMgr
2
+ class ServiceManager
3
+ include Logging
4
+
5
+ def initialize(sink)
6
+ @services = {}
7
+ @sink = sink
8
+
9
+ @service_mutex = Mutex.new
10
+ @sink_mutex = Mutex.new
11
+ end
12
+
13
+ def update_service(name, config)
14
+ logger.info "Received service: #{name}"
15
+
16
+ type = config.delete('type')
17
+ begin
18
+ klass = watcher_class(type)
19
+ @service_mutex.synchronize do
20
+ @services[name].shutdown if @services[name]
21
+ w = @services[name] = klass.new(name, config, self)
22
+ w.watch if w.valid?
23
+ end
24
+ rescue NameError
25
+ logger.warn "Could not find implementation for #{type}. Not adding service #{name}"
26
+ nil
27
+ end
28
+ end
29
+
30
+ def delete_service(name)
31
+ @service_mutex.synchronize do
32
+ svc = @services.delete(name)
33
+ svc.shutdown if svc
34
+ end
35
+ update_backends
36
+ end
37
+
38
+ def update_backends
39
+ @sink_mutex.synchronize { @sink.update_backends @services }
40
+ end
41
+
42
+ def shutdown
43
+ @sink.shutdown
44
+ @services.each { |_name, watcher| watcher.shutdown }
45
+ end
46
+
47
+ private
48
+
49
+ def watcher_class(type)
50
+ Watcher.const_get(type.capitalize)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,100 @@
1
+ module ProxyMgr
2
+ class Sink
3
+ require 'absolute_time'
4
+
5
+ include Logging
6
+
7
+ def initialize(haproxy, opts = {})
8
+ @file = opts[:haproxy_config_file] || '/tmp/haproxy.cfg'
9
+ @default_timeout = opts[:default_timeout] || 2
10
+ @max_timeout = opts[:max_timeout] || 10
11
+ @haproxy = haproxy
12
+ @timeout = nil
13
+ @thread = nil
14
+ @cv = ConditionVariable.new
15
+ @mutex = Mutex.new
16
+ @start_cv = ConditionVariable.new
17
+ @start_mutex = Mutex.new
18
+ @backends = nil
19
+ @haproxy.start
20
+ start
21
+ end
22
+
23
+ def update_backends(backends)
24
+ @mutex.synchronize do
25
+ @backends ||= {}
26
+ backends.each do |name, watcher|
27
+ next if watcher.servers.empty?
28
+ @backends[name] = watcher
29
+ end
30
+ signal
31
+ end
32
+ end
33
+
34
+ def shutdown
35
+ @thread.kill
36
+ @thread.join
37
+ @haproxy.shutdown
38
+ end
39
+
40
+ private
41
+
42
+ def start
43
+ @thread = Thread.new do
44
+ t1 = nil
45
+ @mutex.synchronize do
46
+ loop do
47
+ started! unless started?
48
+
49
+ if @timeout && t1 && AbsoluteTime.now - t1 >= @timeout && @backends
50
+ @haproxy.update_backends(@backends)
51
+
52
+ @timeout = nil
53
+ @backends = nil
54
+ elsif t1
55
+ set_timeout
56
+ logger.debug "Waiting for #{@timeout}s or signal"
57
+ end
58
+
59
+ t1 = AbsoluteTime.now
60
+ logger.debug 'Waiting to be signalled'
61
+ wait
62
+ end
63
+ end
64
+ end
65
+ @thread.abort_on_exception = true
66
+
67
+ wait_for_started
68
+ end
69
+
70
+ def signal
71
+ @cv.signal
72
+ end
73
+
74
+ def wait
75
+ @cv.wait(@mutex, @timeout)
76
+ end
77
+
78
+ def set_timeout
79
+ @timeout = @timeout ? @timeout * @timeout : @default_timeout
80
+ @timeout = @max_timeout if @timeout > @max_timeout
81
+ end
82
+
83
+ def started?
84
+ @start_cv == nil
85
+ end
86
+
87
+ def started!
88
+ @start_mutex.synchronize do
89
+ @start_cv.signal
90
+ @start_cv = nil
91
+ end
92
+ end
93
+
94
+ def wait_for_started
95
+ @start_mutex.synchronize do
96
+ @start_cv.wait(@start_mutex) if @start_cv
97
+ end
98
+ end
99
+ end
100
+ end