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