tdi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ # TDI
2
+
3
+ Test Driven Infrastructure acceptance helpers for validating your deployed
4
+ infrastructure and external dependencies.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'tdi'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install tdi
19
+
20
+ ## Usage
21
+
22
+ $ tdi -h
23
+
24
+ Usage:
25
+
26
+ tdi [options] test_plan_file
27
+
28
+ Examples:
29
+
30
+ tdi tdi.json
31
+ tdi --plan admin tdi.json
32
+ tdi --plan admin::acl tdi.json
33
+ tdi -p admin::acl,admin::file tdi.json
34
+ tdi --nofail tdi.json
35
+ tdi --shred tdi.json
36
+ tdi -v tdi.json
37
+ tdi -vv tdi.json
38
+ tdi -vvv tdi.json
39
+
40
+ Options:
41
+
42
+ -n, --nofail No fail mode.
43
+ -p, --plan Test plan list.
44
+ -s, --shred Wipe out the test plan, leaving no trace behind.
45
+ -v, --verbose Verbose mode.
46
+ -h, --help Display this help message.
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/tdi ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # == TDI
4
+ #
5
+ # Run test plans against the infrastructure, performing tasks such as validating
6
+ # for:
7
+ # - Network access (ACL).
8
+ # - File/directory RO/RW access (Local/NFS).
9
+ # - SSH session capability (Private/Public key pair based auth).
10
+ # - HTTP request/response.
11
+ # TODO: - Successful execution of an arbitrary command.
12
+ # TODO: - DNS resolution.
13
+ #
14
+ # === Authors
15
+ #
16
+ # Rogério Carvalho Schneider <rogerio.schneider@corp.globo.com>
17
+ # Leonardo Martins de Lima <leonardo.martins@corp.globo.com>
18
+ # Diogo Kiss <diogokiss@corp.globo.com>
19
+ # Francisco Corrêa <francisco@corp.globo.com>
20
+ #
21
+ # === Copyright
22
+ #
23
+ # Copyright (C) 2013-2014 Globo.com
24
+
25
+ ##############
26
+ ## REQUIRES ##
27
+ ##############
28
+
29
+ require 'slop'
30
+ require 'json'
31
+ require 'colorize'
32
+ require_relative '../lib/planner'
33
+ require_relative '../lib/runner'
34
+
35
+ ###############
36
+ ## CONSTANTS ##
37
+ ###############
38
+
39
+ UNMERGEABLE_KEY_LIST = %w(desc inherits notest)
40
+ UNTESTABLE_ROLE_LIST = %w(global common)
41
+
42
+ #############
43
+ ## HELPERS ##
44
+ #############
45
+
46
+ # Autoload any helper file inside helper's directory.
47
+ BIN_DIR = File.dirname(File.expand_path(__FILE__))
48
+ Dir["#{BIN_DIR}/../helper/*.rb"].each do |filename|
49
+ filename.sub!(/\.rb$/, '')
50
+ require filename
51
+ end
52
+
53
+ ##########
54
+ ## MAIN ##
55
+ ##########
56
+
57
+ def main(opts)
58
+ # Test plan input file.
59
+ filename = ARGV[0]
60
+
61
+ # Wrong number of command line arguments.
62
+ if filename.nil? and not opts.help?
63
+ puts opts
64
+ exit 1
65
+ end
66
+
67
+ # It is not an error to ask for help.
68
+ exit 0 if opts.help?
69
+
70
+ # Validation.
71
+ validate_args(opts)
72
+
73
+ # Start.
74
+ if opts[:verbose] > 0
75
+ puts "Using \"#{filename}\" as test plan input file.".cyan
76
+ puts
77
+ end
78
+
79
+ # Parse input file.
80
+ original_plan = JSON.parse(open(filename).read)
81
+
82
+ # Print input file.
83
+ if opts[:verbose] > 2
84
+ original_plan.each_pair do |role_name, role_content|
85
+ puts "Found role: #{role_name}".cyan
86
+ puts 'Role content:'.cyan
87
+ puts "* #{role_content}".yellow
88
+ puts
89
+ end
90
+ end
91
+
92
+ # Plan.
93
+ plan = planner(opts, original_plan)
94
+
95
+ # Print test plan.
96
+ if opts[:verbose] > 0
97
+ puts "Test plan built from \"#{filename}\":".cyan
98
+ puts JSON.pretty_generate(plan).yellow
99
+ puts
100
+ end
101
+
102
+ # Run tests.
103
+ runner(opts, filename, plan)
104
+ end
105
+
106
+ # Arguments validation.
107
+ def validate_args(opts)
108
+ puts 'Validating arguments...'.cyan if opts[:verbose] > 1
109
+
110
+ if opts.plan?
111
+ if opts[:plan].nil?
112
+ puts 'ERR: When using test plan filter you must inform at least one test plan name.'.light_magenta
113
+ exit 1
114
+ end
115
+
116
+ opts[:plan].each do |plan_name|
117
+ unless /^\w+(::\w+)?$/.match(plan_name)
118
+ puts "ERR: Invalid test plan filter \"#{plan_name}\". Must match pattern \"role\" or \"role::plan\".".light_magenta
119
+ exit 1
120
+ end
121
+ end
122
+ end
123
+
124
+ if opts[:verbose] > 1
125
+ puts 'Validating arguments... done.'.green
126
+ puts
127
+ end
128
+ end
129
+
130
+ # Command line options.
131
+ begin
132
+ opts = Slop.parse(:help => true, :strict => true) do
133
+ banner <<-EOS
134
+ Usage:
135
+
136
+ tdi [options] test_plan_file
137
+
138
+ Examples:
139
+
140
+ tdi tdi.json
141
+ tdi --plan admin tdi.json
142
+ tdi --plan admin::acl tdi.json
143
+ tdi -p admin::acl,admin::file tdi.json
144
+ tdi --nofail tdi.json
145
+ tdi --shred tdi.json
146
+ tdi -v tdi.json
147
+ tdi -vv tdi.json
148
+ tdi -vvv tdi.json
149
+
150
+ Options:
151
+ EOS
152
+ on :n, :nofail, 'No fail mode.'
153
+ on :p, :plan, 'Test plan list.', :as => Array, :argument => :optional
154
+ on :s, :shred, 'Wipe out the test plan, leaving no trace behind.'
155
+ on :v, :verbose, 'Verbose mode.', :as => :count
156
+ end
157
+ rescue
158
+ puts 'ERR: Invalid option. Try -h or --help for help.'.light_magenta
159
+ exit 1
160
+ end
161
+
162
+ begin
163
+ exit main(opts)
164
+ rescue => e
165
+ puts "ERR: #{e.message}".light_magenta
166
+ exit 1
167
+ end
@@ -0,0 +1,38 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+ require 'etc'
4
+
5
+ class TDIPlan < TDI
6
+ def acl(plan)
7
+ plan.select { |key, val|
8
+ val.is_a?(Hash)
9
+ }.each_pair do |case_name, case_content|
10
+ # Parse.
11
+ host = case_name
12
+ ports = [case_content['port']].flatten
13
+ timeout_limit = case_content['timeout'].nil? ? 1 : case_content['timeout'].to_i
14
+
15
+ # User.
16
+ user = Etc.getpwuid(Process.euid).name
17
+
18
+ # ACL.
19
+ ports.each do |port|
20
+ begin
21
+ timeout(timeout_limit) do
22
+ begin
23
+ sock = TCPSocket.open(host, port)
24
+ sock.close
25
+ success "ACL (#{user}): #{host}:#{port}"
26
+ rescue Errno::ECONNREFUSED
27
+ warning "ACL (#{user}): #{host}:#{port}"
28
+ rescue Errno::ECONNRESET, Errno::ETIMEDOUT
29
+ failure "ACL (#{user}): Connection Refused #{host}:#{port}"
30
+ end
31
+ end
32
+ rescue Timeout::Error
33
+ failure "ACL (#{user}): Timed out (#{timeout_limit}s) #{host}:#{port}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,97 @@
1
+ require 'fileutils'
2
+ require 'etc'
3
+
4
+ class TDIPlan < TDI
5
+ def file(plan)
6
+ plan.select { |key, val|
7
+ val.is_a?(Hash)
8
+ }.each_pair do |case_name, case_content|
9
+ # Parse.
10
+ path = case_name
11
+ user = case_content['user']
12
+ perm = case_content['perm']
13
+ type = case_content['type']
14
+ location = case_content['location']
15
+
16
+ # Flag.
17
+ @flag_success = true
18
+
19
+ # Privileged user.
20
+ begin
21
+ Process.euid = 0
22
+ rescue => e
23
+ puts "ERR: Must run as root to change user credentials #{e}).".light_magenta
24
+ exit 1
25
+ end
26
+
27
+ # Change credentials to local user.
28
+ begin
29
+ Process.euid = Etc.getpwnam(user).uid
30
+ rescue => e
31
+ puts "ERR: User \"#{user}\" not found (#{e}).".light_magenta
32
+ exit 1
33
+ end
34
+
35
+ # Apply the test to a
36
+ def testPerm filename, perm, type
37
+ # Perm.
38
+ begin
39
+ FileUtils.touch(filename)
40
+ @flag_success = false if perm.eql?('ro')
41
+ rescue
42
+ @flag_success = false if perm.eql?('rw')
43
+ ensure
44
+ # Cleanup, if type is directory (remove tempfile).
45
+ FileUtils.rm(filename) if type.eql?('directory') rescue nil
46
+ end
47
+ end
48
+
49
+ # Type.
50
+ case type
51
+ when 'directory'
52
+ # Path.
53
+ filename = "#{path}/#{ENV['HOSTNAME']}.rw"
54
+ testPerm filename, perm, type
55
+ when 'file'
56
+ # Path.
57
+ filename = path
58
+ testPerm filename, perm, type
59
+ when 'link'
60
+ @flag_success = File.symlink?(path)
61
+ @flag_success = File.exist?(path) if @flag_success
62
+ else
63
+ puts "ERR: Invalid file plan format \"#{type}\". Type must be \"directory\", \"file\" or \"link\".".light_magenta
64
+ exit 1
65
+ end
66
+
67
+ # Location.
68
+ unless type.eql?('directory')
69
+ df_path = File.dirname(path)
70
+ else
71
+ df_path = path
72
+ end
73
+ fs_location_query_cmd = "df -P #{df_path} | tail -n 1 | awk '{print $1}'"
74
+ device = `#{fs_location_query_cmd}`
75
+
76
+ case location
77
+ when 'local'
78
+ @flag_success = false if device.include?(':')
79
+ when 'nfs'
80
+ @flag_success = false unless device.include?(':')
81
+ else
82
+ puts "ERR: Invalid file plan format \"#{location}\". Location must be \"local\" or \"nfs\".".light_magenta
83
+ exit 1
84
+ end
85
+
86
+ # Verdict.
87
+ if @flag_success
88
+ success "FILE (#{user}): #{path} => #{perm} #{type} #{location}"
89
+ else
90
+ failure "FILE (#{user}): #{path} => #{perm} #{type} #{location}"
91
+ end
92
+ end
93
+
94
+ # Change credentials back to privileged user.
95
+ Process.euid = 0
96
+ end
97
+ end
@@ -0,0 +1,104 @@
1
+ require 'net/http'
2
+ require "net/https"
3
+ require 'timeout'
4
+ require 'etc'
5
+ require 'uri'
6
+
7
+ class TDIPlan < TDI
8
+
9
+ def _parse(uri,params)
10
+
11
+ # Normalizing
12
+ if not uri =~ /^https?:\/\//
13
+ uri = 'http://' + uri.to_s
14
+ end
15
+
16
+ # URI
17
+ _uri = URI(uri)
18
+ ssl = _uri.scheme.eql?("https")
19
+ host = _uri.host
20
+ port = _uri.port
21
+ path = _uri.path.empty? ? '/' : _uri.path
22
+
23
+ # Params
24
+ code = params['code'].nil? ? 200 : params['code'].to_i
25
+ match = params['match']
26
+ timeout_limit = params['timeout'].nil? ? 2 : params['timeout'].to_i
27
+
28
+ if not params['proxy'].nil?
29
+ proxy_addr, proxy_port = params['proxy'].split(/:/)
30
+ proxy_port = 3128 unless not proxy_port.nil?
31
+ end
32
+
33
+ return host, port, path, proxy_addr, proxy_port, code, match, ssl, timeout_limit
34
+
35
+ end
36
+
37
+ def http(plan)
38
+ plan.select { |key, val|
39
+ val.is_a?(Hash)
40
+ }.each_pair do |case_name,case_content|
41
+
42
+ host, port, path, proxy_addr, proxy_port, code, match, ssl, timeout_limit = _parse(case_name,case_content)
43
+
44
+ # User.
45
+ user = Etc.getpwuid(Process.euid).name
46
+
47
+ response = nil
48
+
49
+ if not proxy_addr.nil? and not proxy_port.nil?
50
+
51
+ http = Net::HTTP::Proxy(proxy_addr, proxy_port)
52
+
53
+ begin
54
+ timeout(timeout_limit) do
55
+ begin
56
+ http.start(host,port,:use_ssl => ssl, :verify_mode => OpenSSL::SSL::VERIFY_NONE) { |http|
57
+ response = http.get(path)
58
+ }
59
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET
60
+ warning "HTTP (#{user}): #{case_name} - Connection reset or refused."
61
+ end
62
+ end
63
+ rescue Timeout::Error
64
+ failure "HTTP (#{user}): #{case_name} - Timed out (#{timeout_limit}s)."
65
+ end
66
+
67
+ else
68
+
69
+ http = Net::HTTP.new(host, port)
70
+
71
+ if ssl
72
+ http.use_ssl = true
73
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
74
+ end
75
+
76
+ begin
77
+ timeout(timeout_limit) do
78
+ begin
79
+ http.start() { |http|
80
+ response = http.get(path)
81
+ }
82
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET
83
+ warning "HTTP (#{user}): #{case_name} - Connection reset or refused."
84
+ end
85
+ end
86
+ rescue Timeout::Error
87
+ failure "HTTP (#{user}): #{case_name} - Timed out."
88
+ end
89
+
90
+ end
91
+
92
+ if not response.nil?
93
+ if not match.nil? and not response.body.chomp.include?(match.chomp)
94
+ failure "HTTP (#{user}): #{case_name} - Expected string '#{match.chomp}'."
95
+ elsif not code.nil? and (response.code.to_i != code)
96
+ failure "HTTP (#{user}): #{case_name} - Expected HTTP #{code}."
97
+ else
98
+ success "HTTP (#{user}): #{case_name}"
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+ end