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