auger 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +11 -0
- data/LICENSE +22 -0
- data/README.md +339 -0
- data/Rakefile +104 -0
- data/VERSION +1 -0
- data/auger.gemspec +26 -0
- data/bin/aug +112 -0
- data/cfg/examples/elasticsearch.rb +75 -0
- data/cfg/examples/redis.rb +31 -0
- data/cfg/examples/riak.rb +42 -0
- data/cfg/examples/webserver.rb +56 -0
- data/lib/auger.rb +12 -0
- data/lib/auger/config.rb +22 -0
- data/lib/auger/connection.rb +39 -0
- data/lib/auger/project.rb +64 -0
- data/lib/auger/request.rb +40 -0
- data/lib/auger/result.rb +27 -0
- data/lib/auger/server.rb +13 -0
- data/lib/auger/test.rb +25 -0
- data/lib/auger/version.rb +3 -0
- data/lib/plugins/cql.rb +31 -0
- data/lib/plugins/dns.rb +44 -0
- data/lib/plugins/http.rb +68 -0
- data/lib/plugins/redis.rb +73 -0
- data/lib/plugins/socket.rb +30 -0
- data/lib/plugins/telnet.rb +37 -0
- metadata +171 -0
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.2.0
|
data/auger.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/auger/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Ric Lister", "Grant Heffernan"]
|
6
|
+
gem.email = ["rlister@gmail.com", "heffergm@gmail.com"]
|
7
|
+
gem.description = %q{Auger: test-driven ops}
|
8
|
+
gem.summary = %q{App && infrastructure testing DSL}
|
9
|
+
gem.homepage = "https://rubygems.org/gems/auger"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "auger"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Auger::VERSION
|
17
|
+
|
18
|
+
# dependencies
|
19
|
+
gem.add_dependency('json' , '>= 1.7.3')
|
20
|
+
gem.add_dependency('redis' , '>= 3.0.1')
|
21
|
+
gem.add_dependency('net-dns' , '>= 0.7.1')
|
22
|
+
gem.add_dependency('rainbow' , '>=1.1.4')
|
23
|
+
gem.add_dependency('host_range' , '>=0.0.1')
|
24
|
+
gem.add_dependency('cassandra-cql' , '>= 1.0.4')
|
25
|
+
end
|
26
|
+
|
data/bin/aug
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
require 'rainbow'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
AUGER_DIR = File.dirname(File.dirname(__FILE__))
|
8
|
+
AUGER_LIB = File.join(AUGER_DIR, 'lib')
|
9
|
+
AUGER_CFG = (ENV['AUGER_CFG'] || File.join(AUGER_DIR, 'cfg')).split(File::PATH_SEPARATOR)
|
10
|
+
|
11
|
+
## relative path to libs (in case not installed as a gem)
|
12
|
+
$LOAD_PATH.unshift(AUGER_LIB) unless $LOAD_PATH.include?(AUGER_LIB)
|
13
|
+
require 'auger'
|
14
|
+
|
15
|
+
## set opts
|
16
|
+
options = {}
|
17
|
+
optparse = OptionParser.new do |opts|
|
18
|
+
opts.banner = "Usage: aug [-h|--help] [-l|--list] [-v|--version] cfg"
|
19
|
+
|
20
|
+
if ARGV[0] == nil
|
21
|
+
puts opts.banner.color(:yellow)
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on('-l', '--list', 'List available configs and exit.') do
|
26
|
+
list = AUGER_CFG.map do |dir|
|
27
|
+
Dir["#{dir}/*.rb"].map{ |file| File.basename(file).sub(/\.rb$/, '') }
|
28
|
+
end
|
29
|
+
puts list.flatten.sort
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on('-h', '--help', 'Display help') do
|
34
|
+
puts opts
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on('-v', '--version', 'Display version and exit.') do
|
39
|
+
puts Auger::VERSION.color(:green)
|
40
|
+
exit
|
41
|
+
end
|
42
|
+
end
|
43
|
+
optparse.parse!
|
44
|
+
|
45
|
+
## load plugins
|
46
|
+
Dir["#{AUGER_DIR}/lib/plugins/*.rb"].each {|file| require file }
|
47
|
+
|
48
|
+
## cfg file can be e.g. 'imagine' or relative path
|
49
|
+
cfg =
|
50
|
+
if File.exists?(ARGV[0])
|
51
|
+
ARGV[0]
|
52
|
+
elsif path = AUGER_CFG.find { |path| File.exists?("#{path}/#{ARGV[0]}.rb") }
|
53
|
+
[path, "#{ARGV[0]}.rb"].join(File::SEPARATOR)
|
54
|
+
else
|
55
|
+
raise ArgumentError, "config #{ARGV[0]} not found"
|
56
|
+
end
|
57
|
+
|
58
|
+
## pretty ascii output for different result outcomes
|
59
|
+
def format_outcome(outcome)
|
60
|
+
case outcome
|
61
|
+
when TrueClass then
|
62
|
+
"\u2713".color(:green)
|
63
|
+
when MatchData then # boolean if no captures, otherwise list captures
|
64
|
+
(outcome.captures.empty? ? "\u2713" : outcome.captures.join(' '))
|
65
|
+
.color(:green)
|
66
|
+
when FalseClass then
|
67
|
+
"\u2717".color(:red)
|
68
|
+
when NilClass then
|
69
|
+
"nil".color(:red)
|
70
|
+
when Exception then
|
71
|
+
"#{outcome.class}: #{outcome.to_s}".color(:magenta)
|
72
|
+
else
|
73
|
+
outcome.to_s.color(:green)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Auger::Config.load(cfg).projects.each do |project|
|
78
|
+
|
79
|
+
threads = Hash.new { |h,k| h[k] = [] }
|
80
|
+
|
81
|
+
## run tests
|
82
|
+
project.connections.each do |connection|
|
83
|
+
project.servers(connection.roles).map do |server|
|
84
|
+
threads[server.name] << Thread.new do
|
85
|
+
conn = connection.do_open(server)
|
86
|
+
connection.requests.map do |request|
|
87
|
+
response = request.do_run(conn)
|
88
|
+
request.tests.map do |test|
|
89
|
+
test.run(response)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
## width of test name column
|
97
|
+
max_test_length =
|
98
|
+
project.connections.map{|c| c.requests.map{|r| r.tests.map{|t| t.name.length}}}.flatten.max
|
99
|
+
|
100
|
+
## print results
|
101
|
+
threads.keys.each do |server|
|
102
|
+
puts "[#{server.color(:cyan)}]"
|
103
|
+
threads[server].each do |thread|
|
104
|
+
results = thread.value # this waits on thread
|
105
|
+
results.flatten.each do |result|
|
106
|
+
output = format_outcome(result.outcome)
|
107
|
+
puts " %+#{max_test_length}s %-30s" % [result.test.name, output]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
project "Elasticsearch" do
|
4
|
+
server "localhost"
|
5
|
+
|
6
|
+
http 9200 do
|
7
|
+
get "/_cluster/health" do
|
8
|
+
|
9
|
+
# this runs after request returns, but before tests
|
10
|
+
# use it to munge response body from json string into a hash
|
11
|
+
before_tests do |r|
|
12
|
+
if r['Content-Type'].respond_to?(:match) and r['Content-Type'].match /application\/json/
|
13
|
+
begin
|
14
|
+
r.body = JSON.parse(r.body)
|
15
|
+
rescue JSON::ParserError
|
16
|
+
puts "error parsing JSON in response body"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# simple as it gets... did we get 200 back?
|
22
|
+
test "Status 200" do |r|
|
23
|
+
r.code == '200'
|
24
|
+
end
|
25
|
+
|
26
|
+
# an array of stats we want to collect
|
27
|
+
stats = %w[
|
28
|
+
cluster_name
|
29
|
+
status
|
30
|
+
timed_out
|
31
|
+
number_of_nodes
|
32
|
+
number_of_data_nodes
|
33
|
+
active_primary_shards
|
34
|
+
active_shards
|
35
|
+
relocating_shards
|
36
|
+
initializing_shards
|
37
|
+
unassigned_shards
|
38
|
+
]
|
39
|
+
|
40
|
+
# loop through each stat
|
41
|
+
# if the body is a hash, return the value
|
42
|
+
stats.each do |stat|
|
43
|
+
test "#{stat}" do |r|
|
44
|
+
if r.body.is_a? Hash
|
45
|
+
r.body[stat]
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# I've discovered that a typical fail case with elasticsearch is
|
53
|
+
# that on occassion, nodes will come up and not join the cluster
|
54
|
+
# This is an easy way to see if the number of nodes that the host
|
55
|
+
# actually sees (actual_data_nodes) matches what we're
|
56
|
+
# expecting (expected_data_nodes).
|
57
|
+
# TODO: dynamically update expected_data_nodes based on defined hosts:
|
58
|
+
test "Expected vs Actual Nodes" do |r|
|
59
|
+
if r.body.is_a? Hash
|
60
|
+
expected_data_nodes = 8
|
61
|
+
actual_data_nodes = r.body['number_of_data_nodes']
|
62
|
+
|
63
|
+
if expected_data_nodes == actual_data_nodes
|
64
|
+
true
|
65
|
+
else
|
66
|
+
false
|
67
|
+
end
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
project "Redis" do
|
2
|
+
server "localhost"
|
3
|
+
|
4
|
+
telnet 6379 do
|
5
|
+
timeout "3"
|
6
|
+
binmode false
|
7
|
+
|
8
|
+
tests = %w[
|
9
|
+
role
|
10
|
+
redis_version
|
11
|
+
uptime_in_days
|
12
|
+
used_memory_human
|
13
|
+
blocked_clients
|
14
|
+
connected_slaves
|
15
|
+
connected_clients
|
16
|
+
]
|
17
|
+
|
18
|
+
|
19
|
+
# issue an info command followed by quit,
|
20
|
+
# otherwise, we'll hang on an open port.
|
21
|
+
|
22
|
+
cmd "info\n\nquit\n\n" do
|
23
|
+
tests.each do |t|
|
24
|
+
test "#{t}" do |r|
|
25
|
+
r.match /#{t}:(.+)/
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
project "Riak" do
|
2
|
+
server "localhost"
|
3
|
+
|
4
|
+
riak_stats = %w[
|
5
|
+
riak_kv_vnodes_running
|
6
|
+
vnode_gets
|
7
|
+
vnode_puts
|
8
|
+
cpu_nprocs
|
9
|
+
]
|
10
|
+
|
11
|
+
http 8098 do
|
12
|
+
get "/stats" do
|
13
|
+
|
14
|
+
riak_stats.each do |t|
|
15
|
+
test "#{t}" do |r|
|
16
|
+
r.body.match /"#{t}":(\d+)/
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
test "CPU Avg 1/5/15" do |r|
|
21
|
+
r.body.match(/"cpu_avg1":(\d+),"cpu_avg5":(\d+),"cpu_avg15":(\d+)/).captures.join("/")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
riak_ports = {
|
27
|
+
epmd_port: 4369,
|
28
|
+
handoff_port: 8099,
|
29
|
+
pb_port: 8087,
|
30
|
+
}
|
31
|
+
|
32
|
+
riak_ports.each do |name, num|
|
33
|
+
socket num do
|
34
|
+
open? do
|
35
|
+
test "#{name} open?" do |r|
|
36
|
+
r
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
project "Webserver Nginx" do
|
2
|
+
server "www.wickedcoolurl.com", :fqdn, :port => 80
|
3
|
+
server "frontend-r[01-04]", :app, :port => 6666
|
4
|
+
server "data-r[01-04]", :data
|
5
|
+
|
6
|
+
http do
|
7
|
+
roles :fqdn, :app
|
8
|
+
|
9
|
+
get "/status" do
|
10
|
+
header "Location: www.wickedcoolurl.com"
|
11
|
+
|
12
|
+
test "Site is up?" do |r|
|
13
|
+
r.body.match /the site is up/
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
https do
|
19
|
+
roles :fqdn
|
20
|
+
|
21
|
+
get "/index.html" do
|
22
|
+
test "Index" do |r|
|
23
|
+
r.body.match /HEAD/
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
https do
|
28
|
+
roles :app
|
29
|
+
insecure true
|
30
|
+
|
31
|
+
get "/index.html" do
|
32
|
+
test "Index" do |r|
|
33
|
+
r.body.match /HEAD/
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
telnet do
|
39
|
+
roles :fqdn, :app
|
40
|
+
timeout "3"
|
41
|
+
binmode false
|
42
|
+
|
43
|
+
cmd "HEAD / HTTP/1.1\n\n" do
|
44
|
+
test "Telnet Port 80" do |r|
|
45
|
+
r.match /Server: (nginx\/[\d\.]+)/
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
socket 9999 do
|
51
|
+
roles :data
|
52
|
+
|
53
|
+
open? { test "Port 9999 is open?" }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/lib/auger.rb
ADDED
data/lib/auger/config.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Auger
|
2
|
+
|
3
|
+
class Config
|
4
|
+
attr_accessor :projects
|
5
|
+
def self.load(filename)
|
6
|
+
config = new
|
7
|
+
config.instance_eval(File.read(filename))
|
8
|
+
config
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@projects = []
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def project(name, &block)
|
17
|
+
@projects << Project.load(name, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Auger
|
2
|
+
|
3
|
+
class Connection
|
4
|
+
attr_accessor :requests, :connection, :response, :roles, :options
|
5
|
+
|
6
|
+
def self.load(port, &block)
|
7
|
+
connection = new(port)
|
8
|
+
connection.instance_eval(&block)
|
9
|
+
connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(port)
|
13
|
+
@options = {:port => port}
|
14
|
+
@roles = []
|
15
|
+
@requests = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def roles(*names)
|
19
|
+
@roles += names if names
|
20
|
+
@roles
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(method, arg)
|
24
|
+
@options[method] = arg
|
25
|
+
end
|
26
|
+
|
27
|
+
## call plugin open() and return plugin-specific connection object, or exception
|
28
|
+
def do_open(server)
|
29
|
+
options = @options.merge(server.options)
|
30
|
+
begin
|
31
|
+
self.open(server.name, options)
|
32
|
+
rescue => e
|
33
|
+
e
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'host_range'
|
2
|
+
|
3
|
+
module Auger
|
4
|
+
|
5
|
+
class Project
|
6
|
+
attr_accessor :name, :fqdns, :hosts, :connections, :roles
|
7
|
+
|
8
|
+
def self.load(name, &block)
|
9
|
+
project = new(name)
|
10
|
+
project.instance_eval(&block)
|
11
|
+
project
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(name)
|
15
|
+
@name = name
|
16
|
+
@hosts = []
|
17
|
+
@fqdns = []
|
18
|
+
@connections = []
|
19
|
+
@roles = Hash.new { |h,k| h[k] = [] }
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def role(name, *args)
|
24
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
25
|
+
servers = args.map { |arg| HostRange.parse(arg) }.flatten
|
26
|
+
#servers.each { |server| roles[name] << server }
|
27
|
+
servers.each { |server| roles[name] << Auger::Server.new(server, options) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def server(*args)
|
31
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
32
|
+
roles = []
|
33
|
+
servers = []
|
34
|
+
args.each do |arg|
|
35
|
+
case arg
|
36
|
+
when Symbol then roles << arg
|
37
|
+
when String then servers << arg
|
38
|
+
else raise ArgumentError, "illegal argument to server: #{arg}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
roles = [nil] if roles.empty? # default role
|
42
|
+
roles.each { |name| role(name, *servers, options) }
|
43
|
+
end
|
44
|
+
|
45
|
+
alias :hosts :server
|
46
|
+
|
47
|
+
## return array of servers for given array of roles (default to all)
|
48
|
+
def servers(roles = [])
|
49
|
+
(roles.empty? ? @roles.values : @roles.values_at(*roles))
|
50
|
+
.flatten
|
51
|
+
end
|
52
|
+
|
53
|
+
alias :host :hosts
|
54
|
+
|
55
|
+
## add fqdn or return list of fqdns
|
56
|
+
def fqdns(*ranges)
|
57
|
+
ranges.empty? ? @fqdns.flatten : @fqdns << [*ranges].map {|r| HostRange.parse(r)}
|
58
|
+
end
|
59
|
+
|
60
|
+
alias :fqdn :fqdns
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|