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,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
|
data/packaging/recipe.rb
ADDED
@@ -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
|
data/proxymgr.gemspec
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|