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