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/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
@@ -0,0 +1,12 @@
1
+ require 'auger/version'
2
+ require 'auger/config'
3
+ require 'auger/project'
4
+ require 'auger/server'
5
+ require 'auger/connection'
6
+ require 'auger/request'
7
+ require 'auger/test'
8
+ require 'auger/result'
9
+
10
+ module Auger
11
+ ##
12
+ end
@@ -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