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