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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a0b2127ca7a38ef6934602e2a632610fb7c76c64
4
+ data.tar.gz: 13bb5a32815c235559e914658dcae62ea2ce7525
5
+ SHA512:
6
+ metadata.gz: 253178319eab3516345259574e70f891bc735a765c82780b6603405b954738a7368ac0c54dacf9bb02a0c30200c1f312e2b5444dfaf1f5cd5465a50dc03df812
7
+ data.tar.gz: 4792f403f75f1110b8d87ff8f83172e32378352625d392f123da5c8d8e999982d4e900cf1c530241ca6a357dca80017a1d863e0be0963b793a91623685173232
@@ -0,0 +1,8 @@
1
+ Dockerfile
2
+ *.swp
3
+ examples/dev.yml
4
+ examples/docker.yml
5
+ coverage/
6
+ packaging/pkg*
7
+ packaging/tmp*
8
+ pkg
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,11 @@
1
+ Style/Documentation:
2
+ Enabled: false
3
+
4
+ Lint/HandleExceptions:
5
+ Enabled: false
6
+
7
+ Lint/RescueException:
8
+ Enabled: false
9
+
10
+ Style/HashSyntax:
11
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org/"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'simplecov', :require => false
7
+ gem 'coveralls', :require => false
8
+ end
@@ -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
@@ -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.
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'bundler/gem_tasks'
3
+
4
+ desc 'Run tests'
5
+ RSpec::Core::RakeTask.new(:spec)
@@ -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
@@ -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 %>
@@ -0,0 +1,5 @@
1
+ ---
2
+ haproxy:
3
+ path: /tmp/haproxy
4
+ config_path: /tmp/haproxy.cfg
5
+ socket_path: /tmp/stats.sock
@@ -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