auger 1.2.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 +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
|