lib splitted in more files. many new features (client, server, packet)

This commit is contained in:
Denis Knauf 2013-04-08 19:46:42 +02:00
parent 2f1c21ff79
commit 476d58d7ec
8 changed files with 512 additions and 309 deletions

10
dummy_server.rb Normal file
View file

@ -0,0 +1,10 @@
require 'socket'
module NSCA
class ServerDummy
attr_reader :server
def initialize *host_and_port
@server = TCPServer.new *host_and_port
end
end
end

View file

@ -2,6 +2,7 @@ require 'socket'
require 'enum'
require 'timeout'
require 'benchmark'
require 'securerandom'
module NSCA
class ReturnCode <Enum
@ -9,294 +10,6 @@ module NSCA
enum %w[OK WARNING CRITICAL UNKNOWN]
end
# This class losly based on send_nsca `SendNsca::NscaConnection`-class.
class Connection
PACKET_VERSION = 3 # NSCA 2.9
# packet-version crc32 timestamp return-code hostname service status(incl perfdata) EOT
PACK_STRING = "n N N n a64 a128 a4096 C"
EOT = 0x17 # Seperator for more than one entry
attr_reader :xor_key, :timestamp, :socket
def initialize socket_or_host, port = nil
@socket = case socket_or_host
when String then Net::TCPSocket.new socket_or_host, port
else socket_or_host
end
# read xor_key and timestamp
xor_key_and_timestamp = @socket.recv 132
@xor_key, ts = xor_key_and_timestamp.unpack 'a128L'
@xor_key_a = @xor_key.unpack 'C*' # needed for every xor
@timestamp = Time.at ts
end
def xor msg
key_a = @xor_key_a
# Slice the message in parts of length key_a.length.
# XOR each char of a part with char at the same index in key_a.
msg.unpack( 'C*').each_slice( key_a.length).inject do |res, part|
res += part.zip( key_a).map {|a,b| a^b }.pack 'C*'
end
end
def crc32 msg
(msg.each_byte.inject 0xFFFFFFFF do |r,b|
8.times.inject( r^b) {|r,_i| (r>>1) ^ (0xEDB88320 * (r&1)) }
end) ^ 0xFFFFFFFF
end
# Builds a check-result-line for NSCA.
#
# Will be terminated by end-of-terminate.
# @param [Time,Integer,nil] timestamp Checked at this time
# @param [0..3] return_code `NSCA::ReturnCode`
# @param [String(length<64),nil] hostname If nil, local hostname will be used.
# Must be known by Nagios.
# @param [String(length<128)] service Name of Service. Must be known by Nagios.
# @param [String(length<4096)] status Status-line inclusive optional Performance Data.
def build_package timestamp, return_code, hostname, service, status
entry = [
PACKET_VERSION, # packet-version
0, # crc32 (unknown yet)
(timestamp || @timestamp).to_i,
return_code.to_i,
hostname || `hostname -f`,
service,
status # incl perfdata
]
# generate crc32 and put it at entry[2...6]
xor "#{entry[0...2]}#{crc32 entry.pack( PACK_STRING)}#{entry[6..-1]}#{EOT.chr}"
end
# Sends a check-result.
# @see #build_package
def send( *a) @socket.write build_package( *a) end
# Sends check-results
# @param [Array<NSCA::Check::Base>] results
def send_results *results
results.flatten.each do |r|
send r.timestamp, r.retcode, r.hostname, r.service, r.text
end
end
# Closes connection to NSCA.
def close( *a) @socket.close( *a) end
end
class Server
attr_reader :socket_or_host, :port, :connect
def initialize socket_or_host = nil, port = nil, &connect
@socket_or_host, @port = socket_or_host, port
@connect = connect || lambda { Connection.new @socket_or_host, @port }
end
def open &e
conn = @connect.call
if block_given?
begin yield conn
ensure conn && conn.close
end
else
conn
end
end
def send *results
open do |conn|
conn.send_results results
end
end
end
module PerformanceData
class Base
extend Timeout
extend Benchmark
def initialize value
@value = value
end
class <<self
attr_reader :label, :unit, :warn, :crit, :min, :max
def init label, unit = nil, warn = nil, crit = nil, min = nil, max = nil
@label, @unit, @warn, @crit, @min, @max = label.to_s, unit, warn, crit, min, max
self
end
def measure &block
timeout ||= 0
exception = Class.new Timeout::Error
pd = perfdatas[perfdata_label]
timeout = pd.max
m = realtime do
begin
timeout timeout, exception, &block
rescue exception
end
end
new m
end
end
attr_reader :value
def label() self.label end
def unit() self.unit end
def warn() self.warn end
def crit() self.crit end
def min() self.min end
def max() self.max end
def return_code
if @value.nil? then 3
elsif crit <= @value then 2
elsif warn <= @value then 1
else 0
end
end
def to_s
"#{label}=#{value}#{unit},#{warn},#{crit},#{min},#{max}"
end
end
class <<self
def create label, unit = nil, warn = nil, crit = nil, min = nil, max = nil
cl = Class.new Base
cl.init label, unit, warn, crit, min, max
end
def new label, unit = nil, warn = nil, crit = nil, min = nil, max = nil
cl = create label, unit, warn, crit, min, max
clname = NSCA::Helper.class_name_gen label
self.const_set clname, cl if clname
cl
end
end
end
module Check
class Base
attr_reader :perfdatas, :return_code, :status, :timestamp
def initialize return_code = nil, status = nil, perfdatas = nil
@perfdatas = {}
init return_code, status, perfdatas, timestamp || Time.now
end
def init return_code = nil, status = nil, perfdatas = nil, timestamp = nil
@return_code = return_code if return_code
@status = status if status
perfdatas.each &method( :[]) if perfdatas
@timestamp = timestamp if timestamp
self
end
def [] perfdata_label
pd = @perfdatas[perfdata_label]
pd && pd.value
end
def []= perfdata_label, value
cl = self.class.perfdatas[perfdata_label]
cl ||= PerformanceData::Base.create perfdata_label
@perfdatas[perfdata_label] = cl.new value
end
def text
r = "#{status || ReturnCode.find(return_code)}"
r += " | #{perfdatas.map( &:to_s).join ' '}" unless perfdatas.empty?
r
end
def measure perfdata_label, &block
@perfdatas[perfdata_label].measure &block
end
def send servers = nil
NSCA.send self, servers
end
def ok status = nil, perfdatas = nil
init ReturnCode::OK, status, perfdatas
send
end
def warning status = nil, perfdatas = nil
init ReturnCode::WARNING, status, perfdatas
send
end
alias warn warning
def critical status = nil, perfdatas = nil
init ReturnCode::CRITICAL, status, perfdatas
send
end
alias crit critical
def unknown status = nil, perfdatas = nil
init ReturnCode::UNKNOWN, status, perfdatas
send
end
def determine_return_code
rc = self.class.perfdatas.map do |label, pdc|
pd = @perfdatas[label]
if pd
pd.return_code
else
-1
end
end.max
end
def retcode
rc = return_code || determine_return_code
(0..3).include?(rc) ? rc : 3
end
class <<self
attr_reader :service, :hostname, :perfdatas
def init service, hostname = nil, perfdatas = nil
@service, @hostname, @perfdatas = service, hostname || `hostname -f`, {}
perfdatas.each {|pd| @perfdatas[pd.label] = pd }
self
end
def ok status = nil, perfdatas = nil
new.ok status, perfdatas
end
def warning status = nil, perfdatas = nil
new.warning status, perfdatas
end
alias warn warning
def critical status = nil, perfdatas = nil
new.warning status, perfdatas
end
alias crit critical
def unknown status = nil, perfdatas = nil
new.unknown status, perfdatas
end
end
end
def create service, hostname = nil, perfdatas = nil
cl = Class.new Base
cl.init service, hostname, perfdatas
cl
end
def new service, hostname = nil, perfdatas = nil
cl = create service, hostname, perfdatas
clname = NSCA::Helper.class_name_gen service
self.const_set clname, cl if clname
cl
end
end
module Helper
class <<self
def class_name_gen label
@ -309,24 +22,16 @@ module NSCA
end
class <<self
def servers() @servers ||= [] end
def destinations() @destinations ||= [] end
def send results, servers = nil
Array.wrap( servers || NSCA.servers).each {|server| server.send results }
def send *results
NSCA.destinations.each {|server| server.send *results }
self
end
end
module Checks
class <<self
def perfdata cl, *params
const_set cl, NSCA::PerformanceData.create( *params)
end
def check cl, service, hostname, perfdatas
perfdatas.map! {|cl| cl.is_a?( Symbol) ? const_get( cl) : cl }
const_set cl, NSCA::Check.create( service, hostname, perfdatas)
end
end
end
end
require 'lib/packet'
require 'lib/server'
require 'lib/client'
require 'lib/check'

167
lib/nsca/check.rb Normal file
View file

@ -0,0 +1,167 @@
module NSCA
module PerformanceData
class Base
extend Timeout
extend Benchmark
class <<self
attr_reader :label, :unit, :warn, :crit, :min, :max
def init label, unit = nil, warn = nil, crit = nil, min = nil, max = nil
@label, @unit, @warn, @crit, @min, @max = label.to_s, unit, warn, crit, min, max
self
end
def measure &block
timeout ||= 0
exception = Class.new Timeout::Error
pd = perfdatas[perfdata_label]
timeout = pd.max
m = realtime do
begin
timeout timeout, exception, &block
rescue exception
end
end
new m
end
end
attr_reader :value
def initialize( value) @value = value end
def label() self.class.label end
def unit() self.class.unit end
def warn() self.class.warn end
def crit() self.class.crit end
def min() self.class.min end
def max() self.class.max end
def to_s() "#{label}=#{value}#{unit},#{warn},#{crit},#{min},#{max}" end
def return_code
if @value.nil? then 3
elsif crit <= @value then 2
elsif warn <= @value then 1
else 0
end
end
end
class <<self
def new label, unit = nil, warn = nil, crit = nil, min = nil, max = nil
cl = Class.new Base
cl.init label, unit, warn, crit, min, max
end
def create label, unit = nil, warn = nil, crit = nil, min = nil, max = nil
cl = new label, unit, warn, crit, min, max
clname = NSCA::Helper.class_name_gen label
self.const_set clname, cl if clname
cl
end
end
end
module Check
class Base
attr_reader :perfdatas, :return_code, :status, :timestamp
def initialize return_code = nil, status = nil, perfdatas = nil
@perfdatas = {}
init return_code, status, perfdatas, timestamp || Time.now
end
def init return_code = nil, status = nil, perfdatas = nil, timestamp = nil
@return_code = return_code if return_code
@status = status if status
perfdatas.each &method( :[]) if perfdatas
@timestamp = timestamp if timestamp
self
end
def [] perfdata_label
pd = @perfdatas[perfdata_label]
pd && pd.value
end
def []= perfdata_label, value
cl = self.class.perfdatas[perfdata_label]
cl ||= PerformanceData::Base.new perfdata_label
@perfdatas[perfdata_label] = cl.new value
end
def text
r = "#{status || ReturnCode.find(return_code)}"
r += " | #{perfdatas.map( &:to_s).join ' '}" unless perfdatas.empty?
r
end
def measure( perfdata_label, &block) @perfdatas[perfdata_label].measure &block end
def send() NSCA::send self end
def ok status = nil, perfdatas = nil
init ReturnCode::OK, status, perfdatas
send
end
def warning status = nil, perfdatas = nil
init ReturnCode::WARNING, status, perfdatas
send
end
alias warn warning
def critical status = nil, perfdatas = nil
init ReturnCode::CRITICAL, status, perfdatas
send
end
alias crit critical
def unknown status = nil, perfdatas = nil
init ReturnCode::UNKNOWN, status, perfdatas
send
end
def determine_return_code
self.class.perfdatas.map do |label, pdc|
pd = @perfdatas[label]
if pd
pd.return_code
else
-1
end
end.max
end
def retcode
rc = return_code || determine_return_code
(0..3).include?(rc) ? rc : 3
end
def service() self.class.service end
def hostname() self.class.hostname end
class <<self
attr_reader :service, :hostname, :perfdatas
def init service, hostname = nil, perfdatas = nil
@service, @hostname, @perfdatas = service, hostname || `hostname -f`, {}
perfdatas.each {|pd| @perfdatas[pd.label] = pd }
self
end
def ok( status = nil, perfdatas = nil) new.ok status, perfdatas end
def warning( status = nil, perfdatas = nil) new.warning status, perfdatas end
alias warn warning
def critical( status = nil, perfdatas = nil) new.warning status, perfdatas end
alias crit critical
def unknown( status = nil, perfdatas = nil) new.unknown status, perfdatas end
end
end
end
module Checks
def perfdata( cl, *params) const_set cl, NSCA::PerformanceData.new( *params) end
def check cl, service, hostname, perfdatas = nil
perfdatas ||= []
perfdatas.map! {|cl| cl.is_a?( Symbol) ? const_get( cl) : cl }
const_set cl, NSCA::Check.new( service, hostname, perfdatas)
end
end
end

100
lib/nsca/client.rb Normal file
View file

@ -0,0 +1,100 @@
require 'socket'
require 'enum'
require 'timeout'
require 'benchmark'
require 'securerandom'
module NSCA
class Client
class Connection
attr_reader :iv_key, :timestamp, :socket, :packet_version, :password
def self.open *args
conn = new *args
if block_given?
begin yield conn
ensure conn && conn.close
end
else conn
end
end
# opts must be a hash
# Connection.new host, port [, opts]
# Connection.new socket [, opts]
# Connection.new host, opts # need `opts = {port: Port}`!
# Connection.new opts # need `opts = {port: Port, hostname: Hostname}`!
# Connection.new opts # need `opts = {port: Port, socket: Socket}`!
def initialize *args
opts = {}
opts = args.pop.dup if args.last.is_a? Hash
opts[:host] ||= opts[:hostname]
opts[:sock] ||= opts[:socket]
opts[:pass] ||= opts[:password]
case args[0]
when String
opts[:host] = args[0]
opts[:port] ||= args[1]
when IO
opts[:sock] = args[0]
end
@socket = if opts[:sock].is_a? IO
opts[:sock]
elsif opts[:host].is_a? String
TCPSocket.new opts[:host], opts[:port]
else
raise ArgumentError, "Socket or hostname+port expected."
end
@packet_version = opts[:packet_version] || PacketV3
# read iv_key and timestamp
iv_key_and_timestamp = @socket.recv 132
@iv_key, ts = iv_key_and_timestamp.unpack 'a128N'
@timestamp = Time.at ts
@password = opts[:pass]
end
# Builds a check-result-line for NSCA.
#
# Will be terminated by end-of-terminate.
# @param [Time,Integer,nil] timestamp Checked at this time
# @param [0..3] return_code `NSCA::ReturnCode`
# @param [String(length<64),nil] hostname If nil, local hostname will be used.
# Must be known by Nagios.
# @param [String(length<128)] service Name of Service. Must be known by Nagios.
# @param [String(length<512)] status Status-line inclusive optional Performance Data.
def build_packet timestamp, return_code, hostname, service, status
packet = @packet_version.new timestamp || @timestamp, return_code, hostname, service, status
packet.build @iv_key
end
# Sends a check-result.
# @see #build_packet
def send_packet( *a) @socket.write build_packet( *a) end
# Sends check-results
# @param [Array<NSCA::Check::Base>] results
def send *results
results.flatten.each do |r|
send r.timestamp, r.retcode, r.hostname, r.service, r.text
end
end
# Closes connection to NSCA.
def close( *a) @socket.close( *a) end
end
attr_reader :socket_or_host, :port, :password
def initialize socket_or_host = nil, port = nil, password = nil, &connect
@socket_or_host, @port, @password = socket_or_host, port, password
end
def open &e
Connection.open @socket_or_host, @port, @password, &e
end
def send( *results) open {|conn| conn.send results } end
end
end

122
lib/nsca/packet.rb Normal file
View file

@ -0,0 +1,122 @@
require 'socket'
require 'enum'
require 'timeout'
require 'benchmark'
require 'securerandom'
module NSCA
class <<self
def xor key, msg, key_a = nil
key_a ||= key.unpack 'C*'
l = key_a.length
return msg if l < 1
# Slice the message in parts of length key_a.length.
# XOR each char of a part with char at the same index in key_a.
msg.unpack( 'C*').each_with_index.map {|c,i| c^key_a[i%l] }.pack 'C*'
end
def crc32 msg
(msg.each_byte.inject 0xFFFFFFFF do |r,b|
8.times.inject( r^b) {|r,_i| (r>>1) ^ (0xEDB88320 * (r&1)) }
end) ^ 0xFFFFFFFF
end
# Builds a null terminated, null padded string of length maxlen
def str2cstr( str, maxlen = nil)
str = str.to_s
str = str.to_s[0..(maxlen-2)] if maxlen
"#{str}\x00"
end
def cstr2str( str, maxlen = nil) str[ 0, x.index( ?\0) || ((maxlen||0)-1)] end
end
class Packet
class CSC32CheckFailed <Exception
end
class VersionCheckFailed <Exception
end
def self.versions version = nil
@@versions ||= {}
version ? @@versions[version] : @@versions
end
def self.register_version( version, klass) versions[version] = klass end
# @param [Time,Integer,nil] timestamp Checked at this time
# @param [0..3] return_code `NSCA::ReturnCode`
# @param [String(length<64),nil] hostname If nil, local hostname will be used.
# Must be known by Nagios.
# @param [String(length<128)] service Name of Service. Must be known by Nagios.
# @param [String(length<512)] status Status-line inclusive optional Performance Data.
def initialize timestamp, return_code, hostname, service, status
@timestamp, @return_code, @hostname, @service, @status =
Time.at( timestamp.to_f), return_code, hostname, service, status
end
attr_accessor :timestamp, :return_code, :hostname, :service, :status
end
class PacketV3 < Packet
NAGIOS_VERSION = 2.7
PACKET_VERSION = 3
END_OF_TRANSMISSION = ?\x0a
HOSTNAME_LENGTH = 64
SERVICE_LENGTH = 128
STATUS_LENGTH = 512
# these line describes the data package:
# typedef struct data_packet_struct{
# int16_t packet_version;
# /* xx means, 2 bytes without any data.
# * i do not know, why but there are 2 extra bytes without any need and
# * no where i can find information how these will be send,
# * because these struct does not know it.
# */
# u_int32_t crc32_value;
# u_int32_t timestamp;
# int16_t return_code;
# char host_name[MAX_HOSTNAME_LENGTH];
# char svc_description[MAX_DESCRIPTION_LENGTH];
# char plugin_output[MAX_PLUGINOUTPUT_LENGTH];
# /* 2 extre xx, too. */
# }data_packet;
PACK_STRING = "s> xx L> L> s> Z#{HOSTNAME_LENGTH} Z#{SERVICE_LENGTH} Z#{STATUS_LENGTH} xx"
PACK_LENGTH = 2+2+4+4+2+HOSTNAME_LENGTH+SERVICE_LENGTH+STATUS_LENGTH+2
register_version PACKET_VERSION, self
# Builds a check-result-line for NSCA.
#
# Will be terminated by end-of-terminate.
def build key = nil, password = nil
entry = [
PACKET_VERSION,
0, # crc32 (unknown yet)
(timestamp || Time.now).to_i,
return_code.to_i,
NSCA::str2cstr( hostname || `hostname -f`, HOSTNAME_LENGTH),
NSCA::str2cstr( service, SERVICE_LENGTH),
NSCA::str2cstr( status, STATUS_LENGTH) # incl perfdata
]
# generate crc32 and put it at entry[2...6]
entry[1] = NSCA::crc32 entry.pack( PACK_STRING)
entry = entry.pack PACK_STRING
entry = NSCA::xor key, entry if key
entry = NSCA::xor password, entry if password
entry
end
def self.parse entry, key = nil, password = nil, no_verification_checks = nil
entry = NSCA::xor key, entry if key
entry = NSCA::xor password, entry if password
p entry: entry
ver, crc32sum, *x = entry.unpack( PACK_STRING)
raise VersionCheckFailed, "Packet version 3 expected. (recv: #{ver})" \
unless no_verification_checks or 3 == ver
entry[4..7] = ?\x00*4
raise CSC32CheckFailed, "crc32-check failed. packet seems to be broken." \
unless no_verification_checks or crc32sum == NSCA::crc32( entry)
new *x
end
end
end

59
lib/nsca/server.rb Normal file
View file

@ -0,0 +1,59 @@
require 'socket'
require 'enum'
require 'timeout'
require 'benchmark'
require 'securerandom'
module NSCA
class Server
attr_reader :iv_key, :server, :packet_version, :password
def initialize *args
opts = {}
opts = args.pop.dup if args.last.is_a? Hash
opts[:host] ||= opts[:hostname]
opts[:sock] ||= opts[:socket]
opts[:pass] ||= opts[:password]
case args[0]
when Integer
opts[:port] = args[0]
opts[:host] ||= args[1]
when IO
opts[:sock] = args[0]
end
@packet_version = opts[:packet_version] || PacketV3
@iv_key = (opts[:iv_key] || SecureRandom.random_bytes( 128)).to_s
raise ArgumentError, "Key must be 128 bytes long" unless 128 == @iv_key.length
@password = opts[:pass].to_s
@server = if opts[:serv].is_a?( TCPServer) or opts[:serv].is_a?( UNIXServer)
opts[:serv]
elsif opts[:port].is_a? Integer
TCPServer.new *[opts[:port], opts[:host]].compact
else
raise ArgumentError, "Server or port-number expected"
end
end
def accept() Connection.new @server.accept, self end
def close() @server.close end
class Connection
def initialize socket, server
@socket, @server = socket, server
@iv_key, @password = server.iv_key, server.password
@packet_version = server.packet_version
@packet_length = @packet_version::PACK_LENGTH
@socket.write [@iv_key, Time.now.to_i].pack( 'a* L>')
end
def fetch
@packet_version.parse read, @iv_key, @password
end
def eof?() @socket.eof? end
def read() @socket.read @packet_length end
def close() @socket.close end
end
end
end

12
test/dummy_server.rb Normal file
View file

@ -0,0 +1,12 @@
module NSCA
def self.dummy_server port, password = nil, key = nil
require 'pathname'
load Pathname.new( __FILE__).dirname.join( '..', 'lib', 'nsca.rb').to_s
serv = NSCA::Server.new port, password: password, key: key
sock = serv.accept
sock.fetch
ensure
sock.close if sock
serv.close if serv
end
end

View file

@ -1,6 +1,34 @@
require 'helper'
class TestNSCA < Test::Unit::TestCase
class TestChecks
extend NSCA::Checks
perfdata :PD1, :pd1_in_sec, :s, 10, 20, 0, 30
perfdata :PD2, :pd2_in_1, 1, 0.99, 0.98, 0, 1
perfdata :PD3, :pd3_count, :c, 3, 5, 0
check :T0, 'TestNSCA0', 'uxnags01-sbe.net.mobilkom.at'
check :T1, 'TestNSCA1', 'uxnags01-sbe.net.mobilkom.at', [PD1, PD2]
check :T2, :TestNSCA2, 'uxnags01-sbe.net.mobilkom.at', [PD1, PD2, PD3]
end
context 'our test server' do
should 'receive data. NSCA-server should run on localhost 5777. if not, ignore this test. password=abcdefghijkl' do
PD1 = TestChecks::PD1
PD2 = TestChecks::PD2
PD3 = TestChecks::PD3
T0 = TestChecks::T0
T1 = TestChecks::T1
T2 = TestChecks::T2
NSCA.destinations << NSCA::Client.new( 'localhost', 5667, 2)
NSCA.send TestChecks::T0.new( 1, "0123456789"*51+"AB")
return
pd1 = PD1.new 3
pd2 = PD2.new 0.9996
pd3 = PD3.new 2
NSCA.send TestChecks::T1.new( nil, "Should be OK", [pd1, pd2, pd3])
end
end
end
class TestNSCA::ReturnCode < Test::Unit::TestCase
@ -26,19 +54,19 @@ end
class TestNSCA::PerformanceData < Test::Unit::TestCase
should 'set a subclass for new PerfData-types' do
NSCA::PerformanceData.new 'subclass test'
NSCA::PerformanceData.create 'subclass test'
assert_nothing_raised NameError do
assert NSCA::PerformanceData::Subclass_test, "No subclass created."
end
end
def perfdata *a
NSCA::PerformanceData.create *a
NSCA::PerformanceData.new *a
end
context 'Created NSCA::PerformanceData-subclasses' do
should 'be the same like returned' do
cl = NSCA::PerformanceData.new 'returned and subclass the same test'
cl = NSCA::PerformanceData.create 'returned and subclass the same test'
assert cl == NSCA::PerformanceData::Returned_and_subclass_the_same_test, 'Classes are not the same.'
end
should 'have a unit if given' do
@ -57,8 +85,8 @@ class TestNSCA::PerformanceData < Test::Unit::TestCase
end
class TestNSCA::Connection < Test::Unit::TestCase
class TestNSCA::Client < Test::Unit::TestCase
should '' do
NSCA::Connection
NSCA::Client
end
end