penctl-ruby 0.1.0
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.
- 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
|