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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a0b2127ca7a38ef6934602e2a632610fb7c76c64
|
4
|
+
data.tar.gz: 13bb5a32815c235559e914658dcae62ea2ce7525
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 253178319eab3516345259574e70f891bc735a765c82780b6603405b954738a7368ac0c54dacf9bb02a0c30200c1f312e2b5444dfaf1f5cd5465a50dc03df812
|
7
|
+
data.tar.gz: 4792f403f75f1110b8d87ff8f83172e32378352625d392f123da5c8d8e999982d4e900cf1c530241ca6a357dca80017a1d863e0be0963b793a91623685173232
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
proxymgr (0.1)
|
5
|
+
absolute_time
|
6
|
+
docopt (~> 0.5.0)
|
7
|
+
state_machine
|
8
|
+
yajl-ruby
|
9
|
+
zookeeper
|
10
|
+
zoology
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: https://rubygems.org/
|
14
|
+
specs:
|
15
|
+
absolute_time (1.0.0)
|
16
|
+
coveralls (0.7.0)
|
17
|
+
multi_json (~> 1.3)
|
18
|
+
rest-client
|
19
|
+
simplecov (>= 0.7)
|
20
|
+
term-ansicolor
|
21
|
+
thor
|
22
|
+
docile (1.1.5)
|
23
|
+
docopt (0.5.0)
|
24
|
+
mime-types (2.3)
|
25
|
+
multi_json (1.10.0)
|
26
|
+
netrc (0.7.7)
|
27
|
+
rest-client (1.7.2)
|
28
|
+
mime-types (>= 1.16, < 3.0)
|
29
|
+
netrc (~> 0.7)
|
30
|
+
simplecov (0.9.0)
|
31
|
+
docile (~> 1.1.0)
|
32
|
+
multi_json
|
33
|
+
simplecov-html (~> 0.8.0)
|
34
|
+
simplecov-html (0.8.0)
|
35
|
+
state_machine (1.2.0)
|
36
|
+
term-ansicolor (1.3.0)
|
37
|
+
tins (~> 1.0)
|
38
|
+
thor (0.19.1)
|
39
|
+
tins (1.3.0)
|
40
|
+
yajl-ruby (1.2.1)
|
41
|
+
zookeeper (1.4.8)
|
42
|
+
zoology (0.1)
|
43
|
+
state_machine
|
44
|
+
zookeeper
|
45
|
+
|
46
|
+
PLATFORMS
|
47
|
+
ruby
|
48
|
+
|
49
|
+
DEPENDENCIES
|
50
|
+
coveralls
|
51
|
+
proxymgr!
|
52
|
+
simplecov
|
data/README.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# ProxyMgr
|
2
|
+
|
3
|
+
ProxyMgr manages Haproxy configuration dynamically. It was built to facilitate communication between services
|
4
|
+
in cloud/dynamic environments where hosts providing a particular service may change frequently. DNS is typically
|
5
|
+
not an option in these environments as most clients will cache resolution of hostnames indefinitely. Other service
|
6
|
+
discovery solutions require integration in your applications, greatly increasing difficulty in adoption.
|
7
|
+
|
8
|
+
ProxyMgr attempts to solve these issues by implementing dynamic reconfiguration of Haproxy. It retrieves service configuration
|
9
|
+
data from Zookeeper and rewrites haproxy.cfg, as well as updating the state of the running process. It avoids reloading
|
10
|
+
the process wherever possible and takes care not to drop connections when a reload is needed.
|
11
|
+
|
12
|
+
## How it works
|
13
|
+
|
14
|
+
ProxyMgr discovers configuration for services (Haproxy frontends/backends) by querying a `service_config` instance. The retrieved
|
15
|
+
configuration is then used to set up a number of `watchers`, which are responsible for finding hosts that make up a service. Current
|
16
|
+
watcher implementations support retrieving service hosts from Zookeeper, DNS, flat files, etc.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
ProxyMgr is available from Rubygems:
|
21
|
+
|
22
|
+
```shell
|
23
|
+
$ gem install proxymgr
|
24
|
+
```
|
25
|
+
|
26
|
+
## Getting started
|
27
|
+
|
28
|
+
ProxyMgr has a configuration file which is used for configuring which service_config to use, as well as defaults for Haproxy. To configure
|
29
|
+
ProxyMgr to retrieve service configuration from Zookeeper you could put this in `proxymgr.yml`:
|
30
|
+
|
31
|
+
```yaml
|
32
|
+
---
|
33
|
+
haproxy:
|
34
|
+
config_path: /etc/haproxy/haproxy.cfg
|
35
|
+
socket_path: /var/run/haproxy/stats.sock
|
36
|
+
|
37
|
+
service_config:
|
38
|
+
type: zookeeper
|
39
|
+
servers: localhost:2181
|
40
|
+
path: /service_config
|
41
|
+
```
|
42
|
+
|
43
|
+
ProxyMgr would then expect to find nodes in /service_config, where the name of the node would be the name of the listen section in Haproxy
|
44
|
+
and the data would contain a JSON blob configuring the watcher for that particular service. The blob could look like this if you would want the
|
45
|
+
watcher to retrieve hosts from Zookeeper:
|
46
|
+
|
47
|
+
```json
|
48
|
+
{"type": "zookeeper",
|
49
|
+
"server": "localhost:2181",
|
50
|
+
"path": "/services/testservice",
|
51
|
+
"listen_options": ["mode http"],
|
52
|
+
"server_options": ["check inter 2000"]}
|
53
|
+
```
|
54
|
+
|
55
|
+
ProxyMgr will now attempt to find nodes describing each server in /services/testservice. Each node should contain a blob looking like this:
|
56
|
+
|
57
|
+
```json
|
58
|
+
{"address": "1.2.3.4",
|
59
|
+
"port": 8080}
|
60
|
+
```
|
61
|
+
|
62
|
+
## Configuration
|
63
|
+
|
64
|
+
ProxyMgr has a main configuration file, `proxymgr.yml`, which is used to configure which service_config to use as well as
|
65
|
+
certain Haproxy options. Each section is a hash of configuration values.
|
66
|
+
|
67
|
+
### `haproxy` section ###
|
68
|
+
|
69
|
+
This section accepts a number of configuration options:
|
70
|
+
|
71
|
+
* `global` should be an array of strings, where each element is a line which will appear in the global section of the Haproxy configuration.
|
72
|
+
* `socket_path` is the path to where the Haproxy stats socket is to be located. ProxyMgr will not be able to enable and disable backends without restarting if this is not supplied.
|
73
|
+
* `path` is the path to the Haproxy binary.
|
74
|
+
|
75
|
+
### `service_config` section ###
|
76
|
+
|
77
|
+
* `type` is the service_config type to use. "zookeeper" is currently the only available option.
|
78
|
+
|
79
|
+
Each service_config has its own configuration keys/values, which should also be put in this section.
|
80
|
+
|
81
|
+
#### `zookeeper` service_config ####
|
82
|
+
|
83
|
+
* `servers` is a list of servers (in format of host:port) separated by commas that should be used to find service configuration
|
84
|
+
* `path` is a path where service configuration nodes can be found.
|
85
|
+
|
86
|
+
## Haproxy management
|
87
|
+
|
88
|
+
ProxyMgr manages the Haproxy process directly; it does not rely on external process managers. This also enables ProxyMgr to provide seamless
|
89
|
+
reloads by opening listen sockets and passing them to Haproxy; as the listen socket remains open in ProxyMgr (the parent process), the kernel
|
90
|
+
will keep accepting connections even in the window between when an old Haproxy process has stopped accepting connections and a new process has not
|
91
|
+
yet begun accepting them.
|
92
|
+
|
93
|
+
ProxyMgr will attempt to avoid reloading Haproxy whenever necessary if stats_socket is configured. This is achieved by disabling and enabling
|
94
|
+
services through the stats socket when they become unavailable/available:
|
95
|
+
|
96
|
+
* If a server is removed, ProxyMgr will disable it through the Haproxy stats socket and write out a new configuration, but not reload the process.
|
97
|
+
* If a server which as previously been removed and disabled is added, ProxyMgr will re-enable it.
|
98
|
+
* If a new server is added, ProxyMgr will add it to the configuration and reload Haproxy.
|
99
|
+
* If a new backend is added, ProxyMgr will add it to the configuration and reload Haproxy.
|
data/Rakefile
ADDED
data/bin/proxymgr
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.expand_path(File.join(__FILE__, '..', '..', 'lib'))
|
3
|
+
|
4
|
+
require 'proxymgr'
|
5
|
+
require 'yaml'
|
6
|
+
require 'docopt'
|
7
|
+
|
8
|
+
Thread.abort_on_exception = true
|
9
|
+
|
10
|
+
opt = <<OPT
|
11
|
+
ProxyMgr manages Haproxy dynamically.
|
12
|
+
|
13
|
+
Usage:
|
14
|
+
#{__FILE__} -c=<path> | --config=<path> [-d|--debug]
|
15
|
+
|
16
|
+
Options:
|
17
|
+
-c=<path> --config=<path> Set configuration file path
|
18
|
+
-d --debug Turn on debug logging
|
19
|
+
OPT
|
20
|
+
|
21
|
+
begin
|
22
|
+
require 'pp'
|
23
|
+
opts = Docopt.docopt(opt)
|
24
|
+
rescue Docopt::Exit => e
|
25
|
+
$stderr.puts e.message
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
ProxyMgr::Logging.level = opts['--debug'] ? Logger::DEBUG : Logger::INFO
|
30
|
+
Zoology::Logging.level = ProxyMgr::Logging.level
|
31
|
+
|
32
|
+
begin
|
33
|
+
config = ProxyMgr::Config.new(opts['--config'])
|
34
|
+
rescue ProxyMgr::Config::ConfigException => e
|
35
|
+
$stderr.puts "config file #{opts['--config']} failed to validate: #{e.message}"
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
|
39
|
+
haproxy_config = config['haproxy']
|
40
|
+
haproxy = ProxyMgr::Haproxy.new(haproxy_config['path'],
|
41
|
+
haproxy_config['config_path'],
|
42
|
+
:socket => haproxy_config['socket_path'],
|
43
|
+
:global => haproxy_config['global'],
|
44
|
+
:defaults => haproxy_config['defaults'])
|
45
|
+
if haproxy.version < 1.5
|
46
|
+
$stderr.puts 'ProxyMgr requires haproxy version 1.5 or later.'
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
|
50
|
+
service_manager = nil
|
51
|
+
begin
|
52
|
+
sink = ProxyMgr::Sink.new(haproxy)
|
53
|
+
service_manager = ProxyMgr::ServiceManager.new(sink)
|
54
|
+
service_config = ProxyMgr::ServiceConfig.create(service_manager,
|
55
|
+
config['service_config'])
|
56
|
+
|
57
|
+
[:INT, :TERM].each do |sig|
|
58
|
+
Signal.trap(sig) do
|
59
|
+
begin
|
60
|
+
Thread.new { service_manager.shutdown }.join
|
61
|
+
rescue Exception => e
|
62
|
+
p e
|
63
|
+
p e.backtrace
|
64
|
+
end
|
65
|
+
exit 0
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
sleep
|
70
|
+
rescue SystemExit
|
71
|
+
rescue NameError, Exception => e
|
72
|
+
service_manager.shutdown if service_manager
|
73
|
+
raise e
|
74
|
+
end
|
data/etc/haproxy.cfg.erb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
global
|
2
|
+
stats socket <%= @socket_path %> mode 666 level admin
|
3
|
+
<% @global_config.each do |line| %> <%= line %>
|
4
|
+
<% end %>
|
5
|
+
defaults
|
6
|
+
<% @defaults_config.each do |line| %> <%= line %>
|
7
|
+
<% end %>
|
8
|
+
<% @backends.each do |name, watcher| %><% next unless @file_descriptors[watcher.port] %>listen <%= name %> fd@<%= @file_descriptors[watcher.port] %><% if watcher.listen_options %><% watcher.listen_options.each do |line| %>
|
9
|
+
<%= line %><% end %><% end %>
|
10
|
+
<% watcher.servers.each do |server| %> server <%= server %> <%= server %><% if watcher.server_options %> <%= watcher.server_options.join(' ') %><% end %>
|
11
|
+
<% end %><% end %>
|
data/examples/config.yml
ADDED
data/lib/proxymgr.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'proxymgr/logging'
|
2
|
+
require 'proxymgr/config'
|
3
|
+
require 'proxymgr/callbacks'
|
4
|
+
require 'proxymgr/service_manager'
|
5
|
+
require 'proxymgr/service_config'
|
6
|
+
require 'proxymgr/process_manager'
|
7
|
+
require 'proxymgr/haproxy'
|
8
|
+
require 'proxymgr/sink'
|
9
|
+
require 'proxymgr/watcher'
|
10
|
+
require 'proxymgr/platform'
|
11
|
+
|
12
|
+
module ProxyMgr
|
13
|
+
def self.root
|
14
|
+
File.expand_path(File.join(__FILE__, '..', '..'))
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.template_dir
|
18
|
+
File.join(root, 'etc')
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ProxyMgr
|
2
|
+
module Callbacks
|
3
|
+
def call(callback, *args)
|
4
|
+
cb = @callbacks[callback]
|
5
|
+
cb.call(*args) if cb
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def callbacks(*callbacks)
|
11
|
+
@callbacks ||= {}
|
12
|
+
callbacks.each do |cb|
|
13
|
+
self.class.send(:define_method, cb) { |&blk| @callbacks[cb] = blk }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module ProxyMgr
|
2
|
+
class Config
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
|
6
|
+
DEFAULTS = {
|
7
|
+
'haproxy' => {
|
8
|
+
'path' => 'haproxy',
|
9
|
+
'config_path' => '/etc/haproxy/haproxy.cfg',
|
10
|
+
'socket_path' => '/var/lib/haproxy.sock',
|
11
|
+
'global' => ['maxconn 4096',
|
12
|
+
'log 127.0.0.1 local0',
|
13
|
+
'log 127.0.0.1 local1 notice'],
|
14
|
+
'defaults' => ['log global',
|
15
|
+
'option dontlognull',
|
16
|
+
'maxconn 2000',
|
17
|
+
'retries 3',
|
18
|
+
'timeout connect 5s',
|
19
|
+
'timeout client 1m',
|
20
|
+
'timeout server 1m',
|
21
|
+
'option redispatch',
|
22
|
+
'balance roundrobin']
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
VALIDATORS = {
|
27
|
+
'haproxy' => {
|
28
|
+
'path' => :executable,
|
29
|
+
'config_path' => :fullpath,
|
30
|
+
'socket_path' => :fullpath,
|
31
|
+
'global' => :array_of_strings,
|
32
|
+
'default' => :array_of_strings
|
33
|
+
},
|
34
|
+
'service_config' => {
|
35
|
+
'type' => :svconfig
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
def initialize(file)
|
40
|
+
data = ERB.new(File.read(file)).result(binding)
|
41
|
+
@config = YAML.load(data) || {}
|
42
|
+
|
43
|
+
merge_defaults!
|
44
|
+
validate_config
|
45
|
+
end
|
46
|
+
|
47
|
+
def [](key)
|
48
|
+
@config[key]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def merge_defaults!
|
54
|
+
DEFAULTS.each do |key, value|
|
55
|
+
if @config[key]
|
56
|
+
@config[key] = value.merge(@config[key])
|
57
|
+
else
|
58
|
+
@config[key] = value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_config
|
64
|
+
validate_haproxy
|
65
|
+
validate_svconfig
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate_svconfig
|
69
|
+
validate_hash(@config['service_config'], VALIDATORS['service_config'])
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_haproxy
|
73
|
+
validate_hash(@config['haproxy'], VALIDATORS['haproxy'])
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_hash(data, validators)
|
77
|
+
fail ConfigException.new "not a hash" unless data.is_a? Hash
|
78
|
+
|
79
|
+
data.each do |key, value|
|
80
|
+
Validators.send(validators[key], key, value) if validators[key]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module Validators
|
85
|
+
class << self
|
86
|
+
def fullpath(key, value)
|
87
|
+
should("#{key} should be a valid full path") { value =~ /^\// }
|
88
|
+
end
|
89
|
+
|
90
|
+
def executable(key, exe)
|
91
|
+
should("#{key} should be an executable") do
|
92
|
+
if exe =~ /^\//
|
93
|
+
File.executable? exe
|
94
|
+
else
|
95
|
+
ENV['PATH'].split(':').find do |e|
|
96
|
+
File.executable? File.join(e, exe)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def array_of_strings(key, ary)
|
103
|
+
ary.each_with_index do |value, i|
|
104
|
+
should("#{key}[#{i}] should be a string") do
|
105
|
+
value.is_a? String
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def svconfig(key, type)
|
111
|
+
should("#{key} should be a service config implementation") do
|
112
|
+
begin
|
113
|
+
ServiceConfig.const_get(type.capitalize)
|
114
|
+
rescue NameError
|
115
|
+
false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def should(reason = nil, &blk)
|
123
|
+
fail ConfigException.new(reason) unless blk.call
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class ConfigException < Exception; end
|
129
|
+
end
|
130
|
+
end
|