tdi 0.1.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.
@@ -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