auger 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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