penctl-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +197 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/lib/pen_balancer.rb +130 -0
- data/lib/penctl.rb +85 -0
- data/penctl-ruby.gemspec +52 -0
- data/rails/init.rb +2 -0
- data/spec/lib/pen_balancer_spec.rb +152 -0
- data/spec/lib/penctl_spec.rb +53 -0
- data/spec/spec_helper.rb +3 -0
- metadata +68 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Jannis Hermanns
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
== Description
|
2
|
+
|
3
|
+
penctl-ruby is a ruby implementation of penctl, a tool to control pen load
|
4
|
+
balancers (a load balancer for "simple" tcp based protocols such as http or smtp): http://siag.nu/pen/
|
5
|
+
|
6
|
+
With penctl-ruby you can add and remove servers to a pen server list and to change settings
|
7
|
+
without the need to restart the pen balancer. Without the need for a penctl binary.
|
8
|
+
|
9
|
+
While pen's regular balancing works fine, issuing commands through the control port
|
10
|
+
can be a bit flakey at times. That's why penctl-ruby has retries built in - it will
|
11
|
+
try to issue your command up to five times. Important commands like add_server() and
|
12
|
+
remove_server() make very sure pen has received them correctly and return true or false
|
13
|
+
accordingly.
|
14
|
+
|
15
|
+
keywords: balancer, pen, penctl, rails, gem
|
16
|
+
|
17
|
+
== Documentation
|
18
|
+
|
19
|
+
RDocs at http://rdoc.info/projects/jayniz/penctl-ruby
|
20
|
+
|
21
|
+
== Installation
|
22
|
+
|
23
|
+
Installing penctl-ruby is quite simple:
|
24
|
+
|
25
|
+
sudo gem install penctl-ruby -s http://gemcutter.org
|
26
|
+
|
27
|
+
If you want it as plugin, just do:
|
28
|
+
|
29
|
+
script/plugin install git://github.com/jayniz/penctl-ruby.git
|
30
|
+
|
31
|
+
== Basic Usage
|
32
|
+
|
33
|
+
Let's say you started a pen balancer on some machine with some command
|
34
|
+
like `pen -C 192.168.0.1:12000 192.168.0.1:12001`. You can then create a
|
35
|
+
PenBalancer object to talk to a pen balancer running on a remote machine:
|
36
|
+
|
37
|
+
>> pen = PenBalancer.new '192.168.0.1:12000'
|
38
|
+
=> #<PenBalancer:0x11eb74c @pen="192.168.0.1:12000">
|
39
|
+
|
40
|
+
If you want to know which servers this pen is balancing requests to, you
|
41
|
+
can issue the servers command:
|
42
|
+
|
43
|
+
>> pen.servers
|
44
|
+
=> [{:conn=>0, :port=>12501, :hard=>0, :addr=>"127.0.0.1", :sx=>89116821, :slot=>0, :rx=>2891017284, :max=>0},
|
45
|
+
{:conn=>0, :port=>12101, :hard=>0, :addr=>"127.0.0.1", :sx=>3178050, :slot=>1, :rx=>46267355, :max=>0},
|
46
|
+
{:conn=>0, :port=>0, :hard=>0, :addr=>"0.0.0.0", :sx=>0, :slot=>2, :rx=>0, :max=>0},
|
47
|
+
{:conn=>0, :port=>0, :hard=>0, :addr=>"0.0.0.0", :sx=>0, :slot=>3, :rx=>0, :max=>0}]
|
48
|
+
|
49
|
+
As you can see, there are still two unused slots left. Let's make use of them!
|
50
|
+
|
51
|
+
== Adding servers to the pool (or removing them)
|
52
|
+
|
53
|
+
Let's add another server to the pool and check if it's there:
|
54
|
+
|
55
|
+
>> pen.add_server '127.0.0.1', 12301
|
56
|
+
=> true
|
57
|
+
>> pen.servers
|
58
|
+
=> [{:conn=>0, :port=>12501, :hard=>0, :addr=>"127.0.0.1", :sx=>89116821, :slot=>0, :rx=>2891017284, :max=>0},
|
59
|
+
{:conn=>0, :port=>12101, :hard=>0, :addr=>"127.0.0.1", :sx=>3178050, :slot=>1, :rx=>46267355, :max=>0},
|
60
|
+
{:conn=>0, :port=>12301, :hard=>0, :addr=>"127.0.0.1", :sx=>0, :slot=>2, :rx=>0, :max=>0},
|
61
|
+
{:conn=>0, :port=>0, :hard=>0, :addr=>"0.0.0.0", :sx=>0, :slot=>3, :rx=>0, :max=>0}]
|
62
|
+
|
63
|
+
Actually you don't have to check if the new server is really there. After attempting to add the server
|
64
|
+
to the pool, it retrieves the server list and makes sure the new server is really there and returns true.
|
65
|
+
If you try to add a server to the pool that exists already, penctl-ruby will complain. Let's try
|
66
|
+
to add the same server again
|
67
|
+
|
68
|
+
>> p.add_server '127.0.0.1', 12301
|
69
|
+
ArgumentError: Server is in the pool already
|
70
|
+
from ./lib/pen_balancer.rb:41:in `add_server'
|
71
|
+
from (irb):15
|
72
|
+
|
73
|
+
To remove a server from the pool, you just call:
|
74
|
+
|
75
|
+
>> pen.remove_server '127.0.0.1', 12301
|
76
|
+
=> true
|
77
|
+
|
78
|
+
== Changing and reading runtime configuration
|
79
|
+
|
80
|
+
There are a couple of readonly and also writeable runtime variables to pen. Most of them are also documented
|
81
|
+
in `man penctl` (or online at http://linux.die.net/man/1/penctl). You can get/set numerical configuration
|
82
|
+
parameters of a remote pen balancer quite easily.
|
83
|
+
|
84
|
+
>> pen.client_acl=3
|
85
|
+
=> 3
|
86
|
+
|
87
|
+
Fetching it goes like this:
|
88
|
+
|
89
|
+
>> pen.client_acl
|
90
|
+
=> 3
|
91
|
+
|
92
|
+
Accordingly, you can set booleans (please note that you cannot get the current value):
|
93
|
+
>> pen.stubborn=false
|
94
|
+
=> false
|
95
|
+
|
96
|
+
There are also values you can only read, but not write:
|
97
|
+
|
98
|
+
>> pen.conn_max
|
99
|
+
=> 256
|
100
|
+
|
101
|
+
Last but not least, you can issue some commands (telling the pen balancer to exit, if you started it
|
102
|
+
with the -X flag, for example):
|
103
|
+
|
104
|
+
>> pen_without_x_flag.exit!
|
105
|
+
=> ["Exit is not enabled; restart with -X flag"]
|
106
|
+
>> pen_with_x_flag.exit!
|
107
|
+
=> []
|
108
|
+
|
109
|
+
== Managing access control lists (ACLs)
|
110
|
+
|
111
|
+
You can define 10 policies to be used for access control. They are organized in slots from
|
112
|
+
0 to 9. To set the ACL in slot 2, you could issue:
|
113
|
+
|
114
|
+
>> pen.set_acl_entry(2, :policy => 'permit', :source_ip => '192.168.0.1', :netmask => '255.255.255.0')
|
115
|
+
|
116
|
+
Now we could configure pen to use this policy for client connections:
|
117
|
+
|
118
|
+
>> p.client_acl=2
|
119
|
+
=> 2
|
120
|
+
|
121
|
+
And to configure pen to use this policy for control port connections (don't lock yourself out :):
|
122
|
+
|
123
|
+
>> p.control_acl=2
|
124
|
+
=> 2
|
125
|
+
|
126
|
+
To remove a policy from the ACL, you would do (effectively allowing all connections):
|
127
|
+
|
128
|
+
>> p.remove_acl_entry 0
|
129
|
+
=> []
|
130
|
+
|
131
|
+
|
132
|
+
== API
|
133
|
+
|
134
|
+
The most important functions are adding/removing and blacklisting/whitelisting servers along with
|
135
|
+
ACL management (see above). The rest of the API is inspired by penctl's manpage:
|
136
|
+
|
137
|
+
=== Boolean (you can only set, but not get, these)
|
138
|
+
|
139
|
+
pen.ascii=true # Communication dumps in ascii format (cf option -a).
|
140
|
+
pen.ascii=false # Communication dumps in hex format.
|
141
|
+
pen.block=true # Do not make sockets nonblocking.
|
142
|
+
pen.block=false # Make sockets nonblocking.
|
143
|
+
pen.delayed_forward=true # Always wait for the next round of the main loop before
|
144
|
+
# forwarding data. Normally pen tries to do that immediately.
|
145
|
+
pen.delayed_forward=false # Try to forward data immediately, to avoid the overhead of
|
146
|
+
# copying it to a temporary buffer and waiting for the next
|
147
|
+
# main loop round.
|
148
|
+
pen.hash=true # Use a hash on the client IP address for initial server selection.
|
149
|
+
pen.hash=false # Do not use a hash.
|
150
|
+
pen.http=true # Add X-Forwarded-For headers to http requests.
|
151
|
+
pen.http=false # Do not add X-Forwarded-For headers.
|
152
|
+
pen.roundrobin=true # Use round-robin server selection without client tracking
|
153
|
+
pen.roundrobin=false # Try to route the same client to the same server over time
|
154
|
+
pen.stubborn=true # If the initial server selection is unavailable, close the
|
155
|
+
# client connection without trying another
|
156
|
+
pen.stubborn=false # If one server does not work, use another
|
157
|
+
pen.weight=true # Use weight for server selection.
|
158
|
+
pen.weight=false # Do not use weight for server selection.
|
159
|
+
|
160
|
+
=== Numeric (you can get and set these)
|
161
|
+
|
162
|
+
pen.blacklist=60 # Set blacklsit time to 60 seconds
|
163
|
+
pen.client_acl=5 # Check connecting clients against access list N (default 0).
|
164
|
+
pen.control_acl=3 # Check accesses to the control port against access list N (default 0).
|
165
|
+
pen.debug=3 # Set debug level to 3
|
166
|
+
pen.timeout=50 # Return current connect timeout in seconds.
|
167
|
+
pen.tracking=5 # Set tracking time, i.e. how long clients will be remembered. The
|
168
|
+
# default 0 will never expire clients based on time
|
169
|
+
|
170
|
+
=== Readonly attributes
|
171
|
+
pen.clients_max # Return max number of clients
|
172
|
+
pen.conn_max # Return max number of simultaneous connections
|
173
|
+
pen.control # Return (address and) port where pen listens for control connections
|
174
|
+
pen.listen # Return (address and) port pen listens to for incoming client connections
|
175
|
+
pen.status # Print status information in html format
|
176
|
+
pen.recent(5) # Shows which clients have connected in the last N seconds (default 300)
|
177
|
+
|
178
|
+
=== Commands
|
179
|
+
pen.exit! # Exit. Only available if pen was started with the -X option
|
180
|
+
pen.include!(filename) # Read commands from filename
|
181
|
+
pen.write!(filename) # Write the current configuration into a file which can be used to
|
182
|
+
# start pen. If FILE is omitted, the configuration is written into
|
183
|
+
# pen's original configuration file
|
184
|
+
|
185
|
+
== TODO
|
186
|
+
|
187
|
+
The :log and :web_stat methods are not yet implemented
|
188
|
+
|
189
|
+
== Bugs and Feedback
|
190
|
+
|
191
|
+
If you discover any bugs or want to drop a line, feel free to email me: On gmail.com
|
192
|
+
my name is jannis.
|
193
|
+
|
194
|
+
penctl-ruby
|
195
|
+
License: MIT
|
196
|
+
Version: 0.1
|
197
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gemspec|
|
9
|
+
gemspec.name = "penctl-ruby"
|
10
|
+
gemspec.summary = "Ruby implementation of penctl"
|
11
|
+
gemspec.description = "With penctl-ruby you can add and remove servers to a pen server list and to change settings without the need to restart the pen balancer."
|
12
|
+
gemspec.email = "jannis@gmail.com"
|
13
|
+
gemspec.homepage = "http://github.com/jayniz/penctl-ruby"
|
14
|
+
gemspec.authors = ["Jannis Hermanns"]
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'Generate documentation for InheritedResources.'
|
21
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
22
|
+
rdoc.rdoc_dir = 'rdoc'
|
23
|
+
rdoc.title = 'InheritedResources'
|
24
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
25
|
+
rdoc.rdoc_files.include('README.rdoc')
|
26
|
+
rdoc.rdoc_files.include('MIT-LICENSE')
|
27
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Run all specs for penctl-ruby"
|
31
|
+
Spec::Rake::SpecTask.new do |t|
|
32
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Default runs specs"
|
36
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/pen_balancer.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
class PenBalancer
|
2
|
+
|
3
|
+
# TODO: :log has log, log=false and log=file
|
4
|
+
# :web_stats
|
5
|
+
|
6
|
+
BOOLEAN_ATTRIBS = [:ascii=, :block=, :control=, :delayed_forward=, :hash=, :http=, :roundrobin=, :stubborn=, :weight=]
|
7
|
+
GETTERS_SETTERS = [:blacklist, :client_acl, :control_acl, :debug, :tracking, :timeout]
|
8
|
+
GETTERS = [:clients_max, :conn_max, :control, :listen, :status, :recent]
|
9
|
+
COMMANDS = [:exit!, :include!, :write!]
|
10
|
+
|
11
|
+
#
|
12
|
+
# Creates a new PenBalancer instance: If you have launched a local pen
|
13
|
+
# instance with 8080 as the control port, you should use PenBalancer
|
14
|
+
# like this: PenBalancer.new 'localhost:8080'.
|
15
|
+
#
|
16
|
+
# Throws an exception when pen can't be reached.
|
17
|
+
#
|
18
|
+
def initialize( address )
|
19
|
+
@pen = address
|
20
|
+
raise "Can't find pen balancer at #{address}" unless Penctl.execute( @pen, "control")[0].match /#{address.split(':')[1]}/
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Returns an array of servers pen currently knows
|
25
|
+
#
|
26
|
+
def servers
|
27
|
+
list = []
|
28
|
+
Penctl.execute(@pen, "servers", 5).each do |l|
|
29
|
+
server = Penctl.parse_server_line(l)
|
30
|
+
list[server[:slot]] = server
|
31
|
+
end
|
32
|
+
list.compact
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Adds a server to the pool or throws an exception when there is no free
|
37
|
+
# slot left
|
38
|
+
#
|
39
|
+
def add_server( host, port )
|
40
|
+
raise ArgumentError.new("Server is in the pool already") if server_in_pool?( host, port)
|
41
|
+
free_slot = get_server( '0.0.0.0', 0 )
|
42
|
+
Penctl.update_server( @pen, free_slot[:slot], :address => host, :port => port)
|
43
|
+
server_in_pool? host, port
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Removes a server from the pool or throws an exception when the server
|
48
|
+
# cannot be found in the pool
|
49
|
+
#
|
50
|
+
def remove_server( host, port )
|
51
|
+
server = get_server( host, port )
|
52
|
+
Penctl.update_server( @pen, server[:slot], :address => '0.0.0.0', :port => 0 )
|
53
|
+
!server_in_pool? host, port
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Blacklists a server for the given amount of seconds or throws an
|
58
|
+
# exception when the server cannot be found in the pool
|
59
|
+
#
|
60
|
+
def blacklist_server( host, port, blacklist_seconds = 3600)
|
61
|
+
server = get_server( host, port )
|
62
|
+
Penctl.update_server( @pen, server[:slot], :blacklist => blacklist_seconds)
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
#
|
67
|
+
# A shortcut for blacklisting a server for 0 seconds.
|
68
|
+
#
|
69
|
+
def whitelist_server( host, port)
|
70
|
+
server = get_server( host, port )
|
71
|
+
Penctl.update_server( @pen, server[:slot], :blacklist => 0)
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Updates an entry of the access control list. Params is a hash with
|
76
|
+
# mandatory :policy => permit/deny and :source_ip. You can optionally
|
77
|
+
# pass a :netmask.
|
78
|
+
#
|
79
|
+
def set_acl_entry( slot, params )
|
80
|
+
raise RangeError.new("slot #{slot} outside range 0-9") unless (0..9).include?(slot)
|
81
|
+
raise ArgumentError.new("policy must be either permit or deny") unless ['permit', 'deny'].include? params[:policy]
|
82
|
+
Penctl.execute(@pen, "acl #{slot} #{params[:policy]} #{params[:source_ip]} #{params[:netmask]}".strip)
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Flushes the rules of an acl entry (==permit from all)
|
87
|
+
#
|
88
|
+
def remove_acl_entry( slot )
|
89
|
+
raise RangeError.new("slot #{slot} outside range 0-9") unless (0..9).include?(slot)
|
90
|
+
Penctl.execute(@pen, "no acl #{slot}")
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# penctl has the following three patterns:
|
95
|
+
# 1) booleans (no getters): penctl localhost:8080 no ascii
|
96
|
+
# penctl localhost:8080 ascii
|
97
|
+
# 2) getters/setters: penctl localhost:8080 debug 3
|
98
|
+
# penctl localhost:8080 debug
|
99
|
+
# 3) commands: penctl localhost:8080 exit
|
100
|
+
# The first two are turned into regular getters and setters and
|
101
|
+
# the last into methods.
|
102
|
+
#
|
103
|
+
def method_missing(method, *args)
|
104
|
+
return Penctl.set_boolean_attribute(@pen, method, args[0]) if BOOLEAN_ATTRIBS.include? method
|
105
|
+
return Penctl.get_set_attribute(@pen, method, args[0]) if GETTERS_SETTERS.include? method.to_s.chomp('=').to_sym
|
106
|
+
return Penctl.get_set_attribute(@pen, method, args[0]) if GETTERS.include? method
|
107
|
+
return Penctl.execute(@pen, "#{method.to_s.chomp('!')} #{args[0]}".strip) if COMMANDS.include? method
|
108
|
+
raise "Missing method #{method}"
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
#
|
114
|
+
# Fetches the list of servers currently known to pen and returns a hash with
|
115
|
+
# :slot, :addr, :port, :conn, :max, :hard, :sx, :rx
|
116
|
+
# Raises an exception when no server with the given address and port is in that list.
|
117
|
+
#
|
118
|
+
def get_server( host, port )
|
119
|
+
server_list = servers
|
120
|
+
server_list.select{ |s| s[:addr] == host and s[:port] == port.to_i}[0] or raise ArgumentError.new("Could not find #{host}:#{port} in #{server_list.inspect}")
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Checks if a given server is already in the pool
|
125
|
+
#
|
126
|
+
def server_in_pool?( host, port )
|
127
|
+
server_list = servers
|
128
|
+
!server_list.select{ |s| s[:addr] == host and s[:port] == port.to_i}.empty?
|
129
|
+
end
|
130
|
+
end
|
data/lib/penctl.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
class Penctl
|
4
|
+
|
5
|
+
#
|
6
|
+
# Connects to pen's control port and issues a command. It will wait a moment and
|
7
|
+
# try to contact the pen server again, when it could not be reached (raising an
|
8
|
+
# exception when it gives up).
|
9
|
+
#
|
10
|
+
def self.execute( server, cmd, tries_left = 5 )
|
11
|
+
raise StandardError.new("Error talking to pen, giving up.") unless tries_left > 0
|
12
|
+
shell_cmd = "penctl #{server} #{cmd}"
|
13
|
+
host, port = server.split ':'
|
14
|
+
begin
|
15
|
+
send_command( host, port, cmd )
|
16
|
+
rescue Errno::ECONNRESET, Errno::EPIPE
|
17
|
+
sleep 0.5
|
18
|
+
return Penctl.execute( server, cmd, tries_left.pred)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Opens a socket, sends a command to pen and returns the result
|
24
|
+
#
|
25
|
+
def self.send_command( host, port, cmd )
|
26
|
+
socket = TCPSocket.open( host, port.to_i )
|
27
|
+
socket.puts cmd
|
28
|
+
result = []
|
29
|
+
while line = socket.gets
|
30
|
+
result << line.chomp
|
31
|
+
end
|
32
|
+
socket.close
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Sends a penctl server command to update a server's settings.
|
38
|
+
#
|
39
|
+
def self.update_server( server, slot, settings )
|
40
|
+
cmd = settings.to_a.sort_by{ |k,v| k.to_s }.flatten.join(' ')
|
41
|
+
Penctl.execute( server, "server #{slot} #{cmd}" )
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
#
|
46
|
+
# Takes a line of the output of penctl's 'servers' command and turns it into
|
47
|
+
# a hash with the keys :slot, :addr:, :port, :conn, :max, :hard, :sx, :rx
|
48
|
+
#
|
49
|
+
def self.parse_server_line( line )
|
50
|
+
keys = %w{ slot addr port conn max hard sx rx}
|
51
|
+
hash = Hash[*("slot #{line}".split)]
|
52
|
+
server = {}
|
53
|
+
keys.each { |k| server[k.to_sym] = (k == "addr") ? hash[k] : hash[k].to_i }
|
54
|
+
return server
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Takes an attribute name along with a boolean value and turns it into
|
59
|
+
# a penctl command. Returns true on success, false on failure.
|
60
|
+
#
|
61
|
+
def self.set_boolean_attribute(pen, attribute, value)
|
62
|
+
cmd = attribute.to_s.chomp '='
|
63
|
+
cmd = value ? cmd : "no " + cmd
|
64
|
+
Penctl.execute(pen, cmd) == ["0"]
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Takes an attribute name along with a value and returns the value.
|
69
|
+
#
|
70
|
+
def self.get_set_attribute(pen, attribute, value = nil)
|
71
|
+
cmd = attribute.to_s.chomp '='
|
72
|
+
value = value.to_s.empty? ? '' : " #{value}"
|
73
|
+
tidy_output(Penctl.execute(pen, "#{cmd}#{value}".chomp))
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def self.tidy_output( values )
|
79
|
+
if values.size == 1
|
80
|
+
values[0].match(/^[\d]+$/) ? values[0].to_i : values[0]
|
81
|
+
else
|
82
|
+
values
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/penctl-ruby.gemspec
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{penctl-ruby}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jannis Hermanns"]
|
12
|
+
s.date = %q{2009-09-28}
|
13
|
+
s.description = %q{With penctl-ruby you can add and remove servers to a pen server list and to change settings without the need to restart the pen balancer.}
|
14
|
+
s.email = %q{jannis@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"MIT-LICENSE",
|
21
|
+
"README.rdoc",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"lib/pen_balancer.rb",
|
25
|
+
"lib/penctl.rb",
|
26
|
+
"penctl-ruby.gemspec",
|
27
|
+
"rails/init.rb",
|
28
|
+
"spec/lib/pen_balancer_spec.rb",
|
29
|
+
"spec/lib/penctl_spec.rb",
|
30
|
+
"spec/spec_helper.rb"
|
31
|
+
]
|
32
|
+
s.homepage = %q{http://github.com/jayniz/penctl-ruby}
|
33
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
s.rubygems_version = %q{1.3.4}
|
36
|
+
s.summary = %q{Ruby implementation of penctl}
|
37
|
+
s.test_files = [
|
38
|
+
"spec/lib/pen_balancer_spec.rb",
|
39
|
+
"spec/lib/penctl_spec.rb",
|
40
|
+
"spec/spec_helper.rb"
|
41
|
+
]
|
42
|
+
|
43
|
+
if s.respond_to? :specification_version then
|
44
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
45
|
+
s.specification_version = 3
|
46
|
+
|
47
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
48
|
+
else
|
49
|
+
end
|
50
|
+
else
|
51
|
+
end
|
52
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'lib/pen_balancer'
|
3
|
+
require 'lib/penctl'
|
4
|
+
|
5
|
+
describe PenBalancer do
|
6
|
+
|
7
|
+
describe "methods implementing the commands from `man penctl` (except for acl and server)" do
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "control").and_return ["127.0.0.1:12000"]
|
11
|
+
@pen = PenBalancer.new '127.0.0.1:12000'
|
12
|
+
end
|
13
|
+
|
14
|
+
it ":servers should return an array of hashes with the servers pen currently knows" do
|
15
|
+
servers_reply = ["0 addr 127.0.0.1 port 12101 conn 0 max 0 hard 0 sx 1054463671 rx 2586728338", "1 addr 127.0.0.1 port 12501 conn 1 max 0 hard 0 sx 1103014051 rx 2688785671"]
|
16
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "servers", 5).and_return servers_reply
|
17
|
+
|
18
|
+
result = @pen.servers
|
19
|
+
result.should have(2).items
|
20
|
+
result[0].should be_a(Hash)
|
21
|
+
result[1].should be_a(Hash)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should set boolean variables (e.g. pen.http=true)" do
|
25
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'http')
|
26
|
+
@pen.http = true
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should set boolean variables (e.g. pen.http=false)" do
|
30
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'no http')
|
31
|
+
@pen.http = false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should set other variables (e.g. pen.debug=5)" do
|
35
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'debug 5').and_return ["5"]
|
36
|
+
@pen.debug = 5
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should get other variables (e.g. pen.debug)" do
|
40
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'debug').and_return ["5"]
|
41
|
+
@pen.debug.should == 5
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should get readonly variables (e.g. pen.status)" do
|
45
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'status').and_return ["lots", "of", "lines"]
|
46
|
+
@pen.status.should == ["lots", "of", "lines"]
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should get readonly variables with a parameter (e.g. pen.recent(5))" do
|
50
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'recent 5').and_return ["lots", "of", "lines"]
|
51
|
+
@pen.recent(5).should == ["lots", "of", "lines"]
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should issue commands (e.g. pen.exit!)" do
|
55
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'exit').and_return []
|
56
|
+
@pen.exit!
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should issue commands with parameters (e.g. pen.write! /etc/pen.conf)" do
|
60
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'write /etc/pen.conf').and_return []
|
61
|
+
@pen.write!('/etc/pen.conf').should be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should return false when a command could not be issued" do
|
65
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", 'exit').and_return ["Exit is not enabled; restart with -X flag"]
|
66
|
+
@pen.exit!
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "methods making the penctl commands [no] acl and server more convenient" do
|
72
|
+
|
73
|
+
describe "adding or removing servers from the pool" do
|
74
|
+
|
75
|
+
before(:each) do
|
76
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "control").and_return ["127.0.0.1:12000"]
|
77
|
+
@pen = PenBalancer.new '127.0.0.1:12000'
|
78
|
+
servers_reply = ["0 addr 127.0.0.1 port 100 conn 0 max 0 hard 0 sx 1054463671 rx 2586728338",
|
79
|
+
"1 addr 127.0.0.1 port 101 conn 1 max 0 hard 0 sx 1103014051 rx 2688785671",
|
80
|
+
"2 addr 0.0.0.0 port 0 conn 0 max 0 hard 0 sx 0 rx 0",
|
81
|
+
"3 addr 0.0.0.0 port 0 conn 0 max 0 hard 0 sx 0 rx 0"]
|
82
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "servers", 5).at_least(1).and_return servers_reply
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should add a server into an empty slot" do
|
86
|
+
Penctl.should_receive(:update_server).with('127.0.0.1:12000', 2, :address => '127.0.0.1', :port => 102)
|
87
|
+
@pen.should_receive(:server_in_pool?).with('127.0.0.1', 102).and_return false
|
88
|
+
@pen.should_receive(:server_in_pool?).with('127.0.0.1', 102).and_return true
|
89
|
+
@pen.add_server('127.0.0.1', 102).should be_true
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should remove a server freeing a slot" do
|
93
|
+
Penctl.should_receive(:update_server).with('127.0.0.1:12000', 1, :address => '0.0.0.0', :port => 0)
|
94
|
+
@pen.should_receive(:server_in_pool?).with('127.0.0.1', 101).and_return false
|
95
|
+
@pen.remove_server('127.0.0.1', 101).should be_true
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should raise an exception when given server could not be found in the list" do
|
99
|
+
lambda {
|
100
|
+
@pen.remove_server('127.0.0.2', 100)
|
101
|
+
}.should raise_error(ArgumentError)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should raise an exception when adding a server that is already in the list" do
|
105
|
+
lambda {
|
106
|
+
@pen.add_server('127.0.0.1', 100)
|
107
|
+
}.should raise_error(ArgumentError)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should blacklist a server" do
|
111
|
+
Penctl.should_receive(:update_server).with('127.0.0.1:12000', 0, :blacklist => 999)
|
112
|
+
@pen.blacklist_server('127.0.0.1', 100, 999)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should whitelist a server" do
|
116
|
+
Penctl.should_receive(:update_server).with('127.0.0.1:12000', 0, :blacklist => 0)
|
117
|
+
@pen.whitelist_server('127.0.0.1', 100)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
describe "managing access control lists" do
|
124
|
+
|
125
|
+
before(:each) do
|
126
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "control").and_return ["127.0.0.1:12000"]
|
127
|
+
@pen = PenBalancer.new '127.0.0.1:12000'
|
128
|
+
end
|
129
|
+
|
130
|
+
it ":set_acl_entry should set access control list with netmask" do
|
131
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "acl 2 permit 192.168.0.1 255.255.255.0")
|
132
|
+
@pen.set_acl_entry(2, :policy => 'permit', :source_ip => '192.168.0.1', :netmask => '255.255.255.0')
|
133
|
+
end
|
134
|
+
|
135
|
+
it ":set_acl_entry should set access control list without netmask" do
|
136
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "acl 2 permit 192.168.0.1")
|
137
|
+
@pen.set_acl_entry(2, :policy => 'permit', :source_ip => '192.168.0.1')
|
138
|
+
end
|
139
|
+
|
140
|
+
it ":set_acl_entry should remove access control (allow from all)" do
|
141
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "no acl 2")
|
142
|
+
@pen.remove_acl_entry(2)
|
143
|
+
end
|
144
|
+
|
145
|
+
it ":set_acl_entry should raise an exception when given ACL list is out of range [0..9]" do
|
146
|
+
lambda {
|
147
|
+
@pen.set_acl_entry(10, :policy => 'deny', :source_ip => '127.0.0.1')
|
148
|
+
}.should raise_error(RangeError)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'lib/penctl'
|
3
|
+
|
4
|
+
describe Penctl do
|
5
|
+
|
6
|
+
it ":execute should call the penctl binary and contact the right pen" do
|
7
|
+
socket = mock("socket")
|
8
|
+
socket.should_receive(:puts).with "foo"
|
9
|
+
socket.should_receive(:gets).and_return "something"
|
10
|
+
socket.should_receive(:gets).and_return false
|
11
|
+
socket.should_receive(:close)
|
12
|
+
TCPSocket.should_receive(:open).with("127.0.0.1", 12000).and_return socket
|
13
|
+
Penctl.execute( "127.0.0.1:12000", "foo").should == ["something"]
|
14
|
+
end
|
15
|
+
|
16
|
+
it ":parse_server_line should turn penctl servers output into a hash" do
|
17
|
+
line = "1 addr 127.0.0.1 port 12501 conn 2709 max 2212 hard 2 sx 1092895943 rx 2664422154"
|
18
|
+
expected = { :slot => 1,
|
19
|
+
:addr => '127.0.0.1',
|
20
|
+
:port => 12501,
|
21
|
+
:conn => 2709,
|
22
|
+
:max => 2212,
|
23
|
+
:hard => 2,
|
24
|
+
:sx => 1092895943,
|
25
|
+
:rx => 2664422154 }
|
26
|
+
Penctl.parse_server_line(line).should == expected
|
27
|
+
end
|
28
|
+
|
29
|
+
it ":update_server should change server settings" do
|
30
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "server 1 address 127.0.0.1 port 88")
|
31
|
+
Penctl.update_server '127.0.0.1:12000', 1, :address => '127.0.0.1', :port => 88
|
32
|
+
end
|
33
|
+
|
34
|
+
it ":set_boolean_attribute should turn true into the right penctl command" do
|
35
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "foo")
|
36
|
+
Penctl.set_boolean_attribute('127.0.0.1:12000', 'foo', true)
|
37
|
+
end
|
38
|
+
|
39
|
+
it ":set_boolean_attribute should turn true into the right penctl command" do
|
40
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "no foo")
|
41
|
+
Penctl.set_boolean_attribute('127.0.0.1:12000', 'foo', false)
|
42
|
+
end
|
43
|
+
|
44
|
+
it ":get_set_attribute should set some attribute's value" do
|
45
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "foo bar").and_return ["0"]
|
46
|
+
Penctl.get_set_attribute('127.0.0.1:12000', 'foo=', 'bar')
|
47
|
+
end
|
48
|
+
|
49
|
+
it ":get_set_attribute should get some attribute's value" do
|
50
|
+
Penctl.should_receive(:execute).with("127.0.0.1:12000", "foo").and_return ["0"]
|
51
|
+
Penctl.get_set_attribute('127.0.0.1:12000', 'foo')
|
52
|
+
end
|
53
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: penctl-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jannis Hermanns
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-28 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: With penctl-ruby you can add and remove servers to a pen server list and to change settings without the need to restart the pen balancer.
|
17
|
+
email: jannis@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- MIT-LICENSE
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- VERSION
|
30
|
+
- lib/pen_balancer.rb
|
31
|
+
- lib/penctl.rb
|
32
|
+
- penctl-ruby.gemspec
|
33
|
+
- rails/init.rb
|
34
|
+
- spec/lib/pen_balancer_spec.rb
|
35
|
+
- spec/lib/penctl_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: http://github.com/jayniz/penctl-ruby
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options:
|
43
|
+
- --charset=UTF-8
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.3.4
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Ruby implementation of penctl
|
65
|
+
test_files:
|
66
|
+
- spec/lib/pen_balancer_spec.rb
|
67
|
+
- spec/lib/penctl_spec.rb
|
68
|
+
- spec/spec_helper.rb
|