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,9 @@
1
+ module ProxyMgr
2
+ module Watcher
3
+ require 'proxymgr/watcher/base'
4
+ require 'proxymgr/watcher/dns'
5
+ require 'proxymgr/watcher/file'
6
+ require 'proxymgr/watcher/zookeeper'
7
+ require 'proxymgr/watcher/campanja_zk'
8
+ end
9
+ end
@@ -0,0 +1,75 @@
1
+ module ProxyMgr
2
+ module Watcher
3
+ class Base
4
+ attr_reader :servers, :port, :listen_options, :server_options
5
+
6
+ include Logging
7
+
8
+ def initialize(name, config, manager)
9
+ @name = name
10
+ @manager = manager
11
+ @config = config
12
+
13
+ @servers = []
14
+ @listen_options = @config['listen_options']
15
+ @server_options = @config['server_options']
16
+ @port = @config['port']
17
+ end
18
+
19
+ def watch
20
+ fail Exception 'This method should be overridden'
21
+ end
22
+
23
+ def shutdown; end
24
+
25
+ def ==(obj)
26
+ if obj.is_a? Watcher::Base
27
+ obj.listen_options == @listen_options and
28
+ obj.server_options == @server_options and
29
+ obj.port == @port
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def valid?
36
+ unless @port
37
+ warn 'port is not defined'
38
+ return false
39
+ end
40
+
41
+ unless @port.is_a? Integer and (@port > 0 and @port <= 65535)
42
+ warn 'port is not an integer or not valid'
43
+ return false
44
+ end
45
+
46
+ unless !@listen_options || @listen_options.is_a?(Array)
47
+ warn 'listen_options is not an array'
48
+ return false
49
+ end
50
+
51
+ unless !@server_options || @server_options.is_a?(Array)
52
+ warn 'server_options is not an array'
53
+ return false
54
+ end
55
+
56
+ if has_validation? and !validate_config
57
+ warn 'config failed to validate'
58
+ return false
59
+ end
60
+
61
+ true
62
+ end
63
+
64
+ private
65
+
66
+ def has_validation?
67
+ respond_to? :validate_config
68
+ end
69
+
70
+ def warn(msg)
71
+ logger.warn "#{@name}: #{msg}. This watcher will not start."
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,20 @@
1
+ module ProxyMgr
2
+ module Watcher
3
+ class Campanjazk < Zookeeper
4
+ def watch_zookeeper(path, type, req)
5
+ if type == :update
6
+ @zk_mapping[path] = ::File.basename(path)
7
+ else
8
+ @zk_mapping.delete(path)
9
+ end
10
+ update_servers(@zk_mapping.values.sort)
11
+ end
12
+
13
+ def update_servers(children)
14
+ servers = children.map { |child| "#{child}:#{@config['port']}" }.sort
15
+ @servers = servers
16
+ @manager.update_backends
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ module ProxyMgr
2
+ module Watcher
3
+ class Dns < Base
4
+ require 'resolv'
5
+
6
+ private
7
+
8
+ def watch
9
+ @thread = Thread.new do
10
+ loop do
11
+ hosts = []
12
+ @config['backends'].map do |backend|
13
+ resolver.each_address(backend['name']) do |addr|
14
+ hosts << "#{addr}:#{backend['port']}"
15
+ end
16
+ end
17
+
18
+ hosts.sort!
19
+
20
+ if @servers != hosts
21
+ @servers = hosts
22
+ @manager.update_backends
23
+ end
24
+
25
+ sleep 5
26
+ end
27
+ end
28
+ @thread.abort_on_exception = true
29
+ end
30
+
31
+ def resolver
32
+ Resolv::DNS.new
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,45 @@
1
+ module ProxyMgr
2
+ module Watcher
3
+ class File
4
+ attr_reader :servers
5
+
6
+ include Logging
7
+
8
+ def initialize(name, config, manager)
9
+ @name = name
10
+ @manager = manager
11
+ @config = config
12
+
13
+ @servers = []
14
+
15
+ @thread = nil
16
+
17
+ watch
18
+ end
19
+
20
+ def shutdown
21
+ end
22
+
23
+ private
24
+
25
+ def watch
26
+ @thread = Thread.new do
27
+ loop do
28
+ if ::File.file? @config['file']
29
+ servers = ::File.readlines(@config['file']).map(&:chomp).sort
30
+ if @servers != servers
31
+ @servers = servers
32
+ @manager.update_backends
33
+ end
34
+ else
35
+ logger.info "#{@name} is not a file, ignoring..."
36
+ end
37
+
38
+ sleep 5
39
+ end
40
+ end
41
+ @thread.abort_on_exception = true
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,61 @@
1
+ module ProxyMgr
2
+ module Watcher
3
+ class Zookeeper < Base
4
+ require 'yajl/json_gem'
5
+ require 'zoology'
6
+
7
+ def watch
8
+ @zookeeper = Zoology::Client.new(@config['server'])
9
+ @path_cache = Zoology::PathCache.new(@zookeeper,
10
+ @config['path'],
11
+ &method(:watch_zookeeper))
12
+ @zk_mapping = {}
13
+ @zookeeper.connect
14
+ end
15
+
16
+ def shutdown
17
+ @zookeeper.close if @zookeeper
18
+ end
19
+
20
+ def validate_config
21
+ unless @config['path'].is_a? String and
22
+ @config['path'] =~ /^\// and
23
+ @config['path'] !~ /\/$/
24
+
25
+ logger.warn "'path' is not a valid Zookeeper path"
26
+ return
27
+ end
28
+
29
+ unless @config['server'].is_a? String and
30
+ @config['server'] =~ /^(?:.*:\d{1,6}){1,}$/
31
+ logger.warn "'server' is not properly specified"
32
+ return
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ private
39
+
40
+ def watch_zookeeper(path, type, req)
41
+ if type == :update
42
+ begin
43
+ config = JSON.parse(req[:data])
44
+ server = "#{config['address']}:#{config['port']}"
45
+ @zk_mapping[path] = server
46
+ rescue Exception => e
47
+ logger.warn "Could not parse config information for backend #{path}: #{e.message}"
48
+ end
49
+ else
50
+ @zk_mapping.delete(path)
51
+ end
52
+ update_servers(@zk_mapping.values.sort)
53
+ end
54
+
55
+ def update_servers(servers)
56
+ @servers = servers
57
+ @manager.update_backends
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1 @@
1
+ export PATH=${PATH}:/opt/proxymgr/bin
@@ -0,0 +1,35 @@
1
+ class ProxyMgr < FPM::Cookery::Recipe
2
+ gemspec = eval(File.read('../proxymgr.gemspec'))
3
+
4
+ homepage 'http://github.com/campanja/proxymgr'
5
+ name 'proxymgr'
6
+ version gemspec.version.to_s
7
+ source 'none', :with => :noop
8
+
9
+ revision '1'
10
+ vendor 'campanja'
11
+ maintainer 'Team Omega <omega@campanja.com>'
12
+ license 'MIT'
13
+
14
+ description 'Manages Haproxy configuration dynamically'
15
+ section 'admin'
16
+
17
+ depends 'ruby2.1', 'haproxy (>= 1.5)'
18
+
19
+ def build
20
+ File.open('Gemfile', 'w') do |fh|
21
+ fh.puts <<-EOF
22
+ source "https://rubygems.org"
23
+
24
+ gem '#{name}', '#{version}', :git => 'git@github.com:campanja/proxymgr.git'
25
+ EOF
26
+ end
27
+ system 'bundle install --binstubs --standalone --path vendor/bundle'
28
+ end
29
+
30
+ def install
31
+ opt('proxymgr').install Dir['*']
32
+ opt('proxymgr').install Dir['.bundle']
33
+ etc('profile.d').install(workdir('profile.sh'), 'proxymgr.sh')
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = 'proxymgr'
3
+ gem.version = '0.1'
4
+ gem.authors = ['Torbjörn Norinder']
5
+ gem.email = ['torbjorn@genunix.se']
6
+ gem.description = %q{Manages Haproxy configuration dynamically}
7
+ gem.summary = gem.description
8
+ gem.homepage = 'https://github.com/campanja/proxymgr'
9
+ gem.platform = Gem::Platform::RUBY
10
+ gem.add_dependency 'docopt', '~> 0.5.0'
11
+ gem.add_dependency 'zoology'
12
+ gem.add_dependency 'absolute_time'
13
+ gem.add_dependency 'yajl-ruby'
14
+ gem.add_dependency 'zookeeper'
15
+ gem.add_dependency 'state_machine'
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ['lib']
20
+ end
@@ -0,0 +1,23 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'proxymgr'
5
+ require 'support/mock_servers'
6
+ require 'support/dummy_watcher'
7
+ require 'support/fake_proxy'
8
+
9
+ ProxyMgr::Logging.disable!
10
+
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+ config.run_all_when_everything_filtered = true
14
+ config.filter_run :focus
15
+
16
+ # Run specs in random order to surface order dependencies. If you find an
17
+ # order dependency and want to debug it, you can fix the order by providing
18
+ # the seed, which is printed after each run.
19
+ # --seed 1234
20
+ config.order = 'random'
21
+ config.include MockServers
22
+ end
23
+
@@ -0,0 +1,21 @@
1
+ module ProxyMgr
2
+ module Watcher
3
+ class Dummy < Base
4
+ attr_reader :name, :config, :manager
5
+ attr_accessor :servers
6
+
7
+ def initialize(name, config, manager, &blk)
8
+ @name = name
9
+ @config = config
10
+ @manager = manager
11
+ @blk = blk
12
+
13
+ super
14
+ end
15
+
16
+ def watch
17
+ @blk.call if @blk
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module ProxyMgr
2
+ class FakeProxy
3
+ def initialize(&blk)
4
+ @blk = blk
5
+ @backends = []
6
+ end
7
+
8
+ def update_backends(backends)
9
+ @backends = backends
10
+ @blk.call
11
+ end
12
+
13
+ def start; end
14
+ end
15
+ end
@@ -0,0 +1,170 @@
1
+ class FakeZookeeper
2
+ require 'zookeeper'
3
+
4
+ def initialize(server, heartbeat, watcher)
5
+ @server = server
6
+ @heartbeat = heartbeat
7
+ @watcher = watcher
8
+
9
+ @connected = true
10
+
11
+ @state = {:children => {}}
12
+ @watches = {}
13
+ end
14
+
15
+ def reopen
16
+ connected!
17
+ end
18
+
19
+ def expired!
20
+ disconnected!
21
+ call_session(Zookeeper::ZOO_EXPIRED_SESSION_STATE)
22
+ end
23
+
24
+ def disconnected!
25
+ call_session(Zookeeper::ZOO_CONNECTING_STATE)
26
+ @connected = false
27
+ end
28
+
29
+ def connected!
30
+ call_session(Zookeeper::ZOO_CONNECTED_STATE)
31
+ @connected = true
32
+ end
33
+
34
+ def connected?
35
+ @connected
36
+ end
37
+
38
+ def get(opts)
39
+ path = opts[:path]
40
+ watcher = opts[:watcher]
41
+
42
+ data = get_path(path)
43
+ if data
44
+ watch_path(path, watcher) if watcher
45
+ event(data)
46
+ else
47
+ event(data, Zookeeper::ZNONODE)
48
+ end
49
+ end
50
+
51
+ def set(opts)
52
+ path = opts[:path]
53
+ data = opts[:data]
54
+
55
+ data = set_path(path, data)
56
+ event = event(data)
57
+ fire_watches(path, event) if data
58
+ event
59
+ end
60
+
61
+ def create(opts)
62
+ path = opts[:path]
63
+ data = opts[:data]
64
+ watcher = opts[:watcher]
65
+
66
+ parts = path.split('/')
67
+ name = parts.pop
68
+ parent = File.join(*parts)
69
+
70
+ data = create_path(name, parent, data)
71
+ event = event(data)
72
+ fire_watches(parent, event) if data
73
+ watch_path(path, watcher) if watcher
74
+ event
75
+ end
76
+
77
+ def get_children(opts)
78
+ path = opts[:path]
79
+ watcher = opts[:watcher]
80
+
81
+ data = get_children_path(path)
82
+ if data
83
+ watch_path(path, watcher) if watcher
84
+ {:rc => Zookeeper::ZOK,
85
+ :children => data}
86
+ else
87
+ {:rc => Zookeeper::ZNONODE}
88
+ end
89
+ end
90
+
91
+ def watcher_callback(&blk)
92
+ blk
93
+ end
94
+
95
+ private
96
+
97
+ def get_path(path)
98
+ node = resolve_node(path)
99
+ (node[:data] || "") if node
100
+ end
101
+
102
+ def set_path(path, set_data)
103
+ node = resolve_node(path)
104
+ node[:data] = set_data if node
105
+ end
106
+
107
+ def watch_path(path, watcher)
108
+ node = resolve_node(path)
109
+ if node
110
+ node[:watches] ||= []
111
+ node[:watches] << watcher
112
+ end
113
+ end
114
+
115
+ def create_path(name, parent, data)
116
+ node = resolve_node(parent)
117
+ if node
118
+ c = node[:children] ||= {}
119
+ n = c[name] ||= {}
120
+ if data
121
+ n[:data] = data
122
+ else
123
+ true
124
+ end
125
+ end
126
+ end
127
+
128
+ def get_children_path(path)
129
+ node = resolve_node(path)
130
+ node[:children] ? node[:children].keys : [] if node
131
+ end
132
+
133
+ def fire_watches(path, event = nil)
134
+ node = resolve_node(path)
135
+ node.delete(:watches).each { |w| w.call event } if node and node[:watches]
136
+ end
137
+
138
+ def resolve_node(path)
139
+ parts = path.split('/')
140
+ return @state if parts.size <= 1
141
+ parts.shift
142
+ leaf_name = parts.pop
143
+ r = parts.inject(@state[:children]) do |state, comp|
144
+ data = state[comp]
145
+ break unless data and data[:children]
146
+ data[:children]
147
+ end
148
+ r[leaf_name] if r
149
+ end
150
+
151
+ def call_session(state)
152
+ @watcher.call(zoo_session_event(state))
153
+ end
154
+
155
+ def zoo_session_event(state)
156
+ Event.new.tap do |e|
157
+ e.state = state
158
+ e.type = state
159
+ end
160
+ end
161
+
162
+ def event(data, rc = Zookeeper::ZOK)
163
+ Event.new.tap do |e|
164
+ e.data = data
165
+ e.rc = rc
166
+ end
167
+ end
168
+
169
+ class Event < Struct.new(:rc, :state, :data, :type); end
170
+ end