cem_acpt 0.7.3 → 0.8.1

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.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+ require 'securerandom'
5
+ require_relative 'action_result'
3
6
  require_relative 'goss'
4
7
  require_relative 'logging'
5
8
  require_relative 'platform'
@@ -17,14 +20,17 @@ module CemAcpt
17
20
  include CemAcpt::Logging
18
21
 
19
22
  attr_reader :duration, :exit_code
23
+ attr_accessor :run_data # This is opened up mainly for windows use.
20
24
 
21
25
  def initialize(config)
22
26
  @config = config
23
27
  @run_data = {}
24
28
  @duration = 0
25
29
  @exit_code = 0
26
- @results = nil
30
+ @results = []
27
31
  @http_statuses = []
32
+ @provisioned = false
33
+ @destroyed = false
28
34
  end
29
35
 
30
36
  def inspect
@@ -38,149 +44,222 @@ module CemAcpt
38
44
  def run
39
45
  @run_data = {}
40
46
  @start_time = Time.now
41
- logger.with_ci_group("CemAcpt v#{CemAcpt::VERSION} run started at #{@start_time}") do
42
- logger.info('CemAcpt') { "Using module directory: #{config.get('module_dir')}..." }
43
- Dir.chdir(config.get('module_dir')) do
44
- keep_terminal_alive
45
- @run_data[:private_key], @run_data[:public_key], @run_data[:known_hosts] = new_ephemeral_ssh_keys
46
- logger.info('CemAcpt') { 'Created ephemeral SSH key pair...' }
47
- @run_data[:module_package_path] = build_module_package
48
- logger.info('CemAcpt') { "Created module package: #{@run_data[:module_package_path]}..." }
49
- @run_data[:test_data] = new_test_data
50
- logger.info('CemAcpt') { 'Created test data...' }
51
- logger.verbose('CemAcpt') { "Test data: #{@run_data[:test_data]}" }
52
- @run_data[:nodes] = new_node_data
53
- logger.info('CemAcpt') { 'Created node data...' }
54
- logger.verbose('CemAcpt') { "Node data: #{@run_data[:nodes]}" }
55
- @instance_names_ips = provision_test_nodes
56
- logger.info('CemAcpt') { 'Provisioned test nodes...' }
57
- logger.debug('CemAcpt') { "Instance names and IPs: #{@instance_names_ips}" }
58
- @results = run_tests(@instance_names_ips.map { |_, v| v['ip'] },
59
- config.get('actions.only'),
60
- config.get('actions.except'))
47
+ module_dir = config.get('module_dir')
48
+ @old_dir = Dir.pwd
49
+ Dir.chdir(module_dir)
50
+ logger.start_ci_group("CemAcpt v#{CemAcpt::VERSION} run started at #{@start_time}")
51
+ logger.info('CemAcpt::TestRunner') { "Using module directory: #{module_dir}..." }
52
+ @run_data[:private_key], @run_data[:public_key], @run_data[:known_hosts] = new_ephemeral_ssh_keys
53
+ logger.info('CemAcpt::TestRunner') { 'Created ephemeral SSH key pair...' }
54
+ @run_data[:module_package_path] = build_module_package
55
+ logger.info('CemAcpt::TestRunner') { "Created module package: #{@run_data[:module_package_path]}..." }
56
+ @run_data[:test_data] = new_test_data
57
+ logger.info('CemAcpt::TestRunner') { 'Created test data...' }
58
+ logger.verbose('CemAcpt::TestRunner') { "Test data: #{@run_data[:test_data]}" }
59
+ @run_data[:nodes] = new_node_data
60
+ logger.info('CemAcpt::TestRunner') { 'Created node data...' }
61
+ logger.verbose('CemAcpt::TestRunner') { "Node data: #{@run_data[:nodes]}" }
62
+ @instance_names_ips = provision_test_nodes
63
+ logger.info('CemAcpt::TestRunner') { "Instance names and IPs class: #{@instance_names_ips.class}" }
64
+ @provisioned = true
65
+ logger.info('CemAcpt::TestRunner') { 'Provisioned test nodes...' }
66
+ logger.debug('CemAcpt::TestRunner') { "Instance names and IPs: #{@instance_names_ips}" }
67
+ # Verifying that we're running on windows nodes or not
68
+ if config.get('tests').first.include? 'windows'
69
+ logger.info('CemAcpt') { 'Running on windows nodes...' }
70
+ upload_module_to_bucket
71
+
72
+ @instance_names_ips.each do |k, v|
73
+ # Login_info here is basically a super charged version of a hash from
74
+ # instance_names_ips. It contains the username, password, and ip of the
75
+ # windows node, as well as the test name that will be run on that node.
76
+ login_info = CemAcpt::Utils.get_windows_login_info(k, v)
77
+ win_node = CemAcpt::Utils::WinRMRunner::WinNode.new(login_info, @run_data[:win_remote_module_name])
78
+ win_node.run
61
79
  end
62
80
  end
81
+ @results = run_tests(@instance_names_ips.map { |_, v| v['ip'] },
82
+ config.get('actions.only'),
83
+ config.get('actions.except'))
84
+ rescue StandardError => e
85
+ logger.error('CemAcpt::TestRunner') { 'Run failed due to error...' }
86
+ @results << ActionResult.new(e)
63
87
  ensure
88
+ logger.end_ci_group
64
89
  clean_up
65
90
  process_test_results
91
+ Dir.chdir(@old_dir) if @old_dir
92
+ @results
66
93
  end
67
94
 
68
- def clean_up(trap_context = false)
69
- kill_keep_terminal_alive unless trap_context
70
-
71
- return no_destroy if config.get('no_destroy_nodes')
95
+ def clean_up(_trap_context = false)
96
+ logger.start_ci_group("CemAcpt v#{CemAcpt::VERSION} run finished at #{Time.now}")
97
+ logger.debug('CemAcpt::TestRunner') { "Starting clean up, provisioned: #{@provisioned}, destroyed: #{@destroyed}" }
72
98
 
73
- clean_ephemeral_ssh_keys
74
- destroy_test_nodes
99
+ if config.get('no_destroy_nodes')
100
+ logger.warn('CemAcpt::TestRunner') { 'Not destroying test nodes because no-destroy-nodes is set...' }
101
+ @provisioner&.show
102
+ logger.info('CemAcpt') { "Test SSH Keys:\n Private Key: #{@run_data[:private_key]}\n Public Key:#{@run_data[:public_key]}" }
103
+ else
104
+ cleanup_bucket # Clean up bucket if we're testing the cem_windows module
105
+ clean_ephemeral_ssh_keys
106
+ destroy_test_nodes
107
+ end
108
+ rescue StandardError => e
109
+ logger.verbose('CemAcpt::TestRunner') { "Error cleaning up: #{e}" }
110
+ logger.verbose('CemAcpt::TestRunner') { e.backtrace.join("\n") }
111
+ ensure
112
+ logger.end_ci_group
75
113
  end
76
114
 
77
115
  private
78
116
 
79
117
  attr_reader :config
80
118
 
81
- # @return [Thread] The thread that keeps the terminal alive
82
- def keep_terminal_alive
83
- return unless config.ci?
84
-
85
- @keep_terminal_alive ||= CemAcpt::Utils::Terminal.keep_terminal_alive
86
- end
87
-
88
- def kill_keep_terminal_alive
89
- return if @trap_context
90
-
91
- keep_terminal_alive&.kill
92
- end
93
-
94
119
  # @return [String] The path to the module package
95
120
  def build_module_package
96
- CemAcpt::Utils::Puppet.build_module_package(config.get('module_dir'))
121
+ if config.get('tests').first.include? 'windows'
122
+ CemAcpt::Utils.package_win_module(config.get('module_dir'))
123
+ else
124
+ CemAcpt::Utils::Puppet.build_module_package(config.get('module_dir'))
125
+ end
97
126
  end
98
127
 
99
128
  # @return [Array<String>] The paths to the ssh private key, public key, and known hosts file
100
129
  def new_ephemeral_ssh_keys
101
130
  return [nil, nil, nil] if config.get('no_ephemeral_ssh_key')
102
131
 
132
+ logger.info('CemAcpt::TestRunner') { 'Creating ephemeral SSH keys...' }
103
133
  CemAcpt::Utils::SSH::Ephemeral.create
104
134
  end
105
135
 
106
136
  def clean_ephemeral_ssh_keys
107
137
  return if config.get('no_ephemeral_ssh_key') || config.get('no_destroy_nodes')
108
138
 
139
+ logger.info('CemAcpt::TestRunner') { 'Cleaning ephemeral SSH keys...' }
109
140
  CemAcpt::Utils::SSH::Ephemeral.clean
110
141
  end
111
142
 
112
143
  def new_test_data
144
+ logger.debug('CemAcpt::TestRunner') { 'Creating new test data...' }
113
145
  CemAcpt::TestData.acceptance_test_data(config)
114
146
  end
115
147
 
116
148
  def new_node_data
149
+ logger.debug('CemAcpt::TestRunner') { 'Creating new node data...' }
117
150
  CemAcpt::Platform.use(config.get('platform.name'), config, @run_data)
118
151
  end
119
152
 
120
153
  def provision_test_nodes
154
+ logger.info('CemAcpt::TestRunner') { 'Provisioning test nodes...' }
121
155
  @provisioner = CemAcpt::Provision.new_provisioner(config, @run_data)
122
156
  @provisioner.provision
123
157
  end
124
158
 
125
159
  def destroy_test_nodes
126
- return no_destroy if config.get('no_destroy_nodes')
127
-
128
- logger.with_ci_group("CemAcpt v#{CemAcpt::VERSION} run finished at #{Time.now}") { @provisioner&.destroy }
160
+ logger.info('CemAcpt::TestRunner') { 'Destroying test nodes if necessary...' }
161
+ if !@provisioned
162
+ logger.warn('CemAcpt::TestRunner') { 'Test nodes not provisioned, nothing to destroy...' }
163
+ elsif @destroyed
164
+ logger.warn('CemAcpt::TestRunner') { 'Test nodes already destroyed, not destroying...' }
165
+ else
166
+ logger.info('CemAcpt::TestRunner') { 'Test nodes are provisioned and not destroyed, destroying...' }
167
+ @provisioner&.destroy
168
+ @destroyed = true
169
+ end
129
170
  end
130
171
 
131
172
  def no_destroy
132
- logger.warn('CemAcpt') { 'Not destroying test nodes...' }
133
- logger.with_ci_group("CemAcpt v#{CemAcpt::VERSION} run finished at #{Time.now}") do
134
- @provisioner&.show
135
- logger.info('CemAcpt') { "Test SSH Keys:\n Private Key: #{@run_data[:private_key]}\n Public Key:#{@run_data[:public_key]}" }
136
- end
173
+ logger.warn('CemAcpt::TestRunner') { 'Not destroying test nodes because no-destroy-nodes is set...' }
174
+ @provisioner&.show
175
+ logger.info('CemAcpt') { "Test SSH Keys:\n Private Key: #{@run_data[:private_key]}\n Public Key:#{@run_data[:public_key]}" }
137
176
  end
138
177
 
139
178
  def run_tests(hosts, only_actions, except_actions)
140
- only_actions = [] if only_actions.nil?
141
- except_actions = [] if except_actions.nil?
142
- CemAcpt::Goss::Api.run_actions_async(hosts, only: only_actions, except: except_actions)
143
- end
144
-
145
- def result_log_formatter
146
- @result_log_formatter ||= LogFormatter::GossActionResponse.new(config, @instance_names_ips)
179
+ logger.info('CemAcpt::TestRunner') { 'Running tests...' }
180
+ logger.verbose('CemAcpt::TestRunner') { "Hosts: #{hosts}" }
181
+ logger.verbose('CemAcpt::TestRunner') { "Only actions: #{only_actions}" }
182
+ logger.verbose('CemAcpt::TestRunner') { "Except actions: #{except_actions}" }
183
+ api_results = CemAcpt::Goss::Api.run_actions_async(hosts,
184
+ only: only_actions || [],
185
+ except: except_actions || [])
186
+ res = []
187
+ api_results.close unless api_results.closed?
188
+ while (r = api_results.pop)
189
+ res << ActionResult.new(r)
190
+ end
191
+ res
147
192
  end
148
193
 
149
194
  def process_test_results
150
- if @results.nil?
151
- logger.error('CemAcpt') { 'No test results to process' }
195
+ if @results.nil? || @results.empty?
196
+ logger.error('CemAcpt::TestRunner') { 'No test results to process' }
152
197
  @exit_code = 1
153
198
  else
199
+ logger.info('CemAcpt::TestRunner') { "Processing #{@results.size} test result(s)..." }
154
200
  until @results.empty?
155
201
  result = @results.pop
156
202
  @http_statuses << result.http_status
157
203
  log_test_result(result)
158
204
  end
159
205
  if @http_statuses.empty?
160
- logger.error('CemAcpt') { 'No test results to process' }
206
+ logger.error('CemAcpt::TestRunner') { 'No test results to process' }
161
207
  @exit_code = 1
162
208
  else
163
- @exit_code = @http_statuses.any? { |s| s.to_i != 200 } ? 1 : 0
209
+ @exit_code = (@http_statuses.any? { |s| s.to_i != 200 }) ? 1 : 0
164
210
  end
165
211
  end
166
212
  @duration = Time.now - @start_time
167
- logger.info('CemAcpt') { "Test suite finished after ~#{duration.round} seconds." }
213
+ logger.info('CemAcpt::TestRunner') { "Test suite finished after ~#{duration.round} seconds." }
168
214
  end
169
215
 
170
216
  def log_test_result(result)
171
- logger.with_ci_group("Test results for #{result_log_formatter.test_name(result)}") do
172
- logger.info(result_log_formatter.summary(result))
173
- formatted_results = result_log_formatter.results(result)
174
- formatted_results.each do |r|
175
- if r.start_with?('Passed:')
176
- logger.verbose { r }
177
- elsif r.start_with?('Skipped:')
178
- logger.info { r }
179
- else
180
- logger.error { r }
181
- end
217
+ result_log_formatter = LogFormatter.new_formatter(result, config, @instance_names_ips)
218
+ logger.start_ci_group("Test results for #{result_log_formatter.test_name(result)}")
219
+ return log_error_test_result(result_log_formatter, result) if result.error?
220
+
221
+ logger.info(result_log_formatter.summary(result))
222
+ formatted_results = result_log_formatter.results(result)
223
+ formatted_results.each do |r|
224
+ if r.start_with?('Passed:')
225
+ logger.verbose { r }
226
+ elsif r.start_with?('Skipped:')
227
+ logger.info { r }
228
+ else
229
+ logger.error { r }
182
230
  end
183
231
  end
232
+ ensure
233
+ logger.end_ci_group
234
+ end
235
+
236
+ def log_error_test_result(formatter, result)
237
+ logger.fatal { formatter.results(result).join("\n") }
238
+ end
239
+
240
+ # Upload the cem_windows module to the bucket if we're testing the cem_windows module
241
+ # This should only be done once per cem_acpt run. It's important to update the module_package_path
242
+ # in the run_data to reflect the new module path if we do end up changing the module name
243
+ def upload_module_to_bucket
244
+ @run_data[:win_remote_module_name] = SecureRandom.uuid << File.split(@run_data[:module_package_path]).last
245
+ @run_data[:win_remote_module_path] = File.join('gs://win_cem_acpt', @run_data[:win_remote_module_name])
246
+ # Upload the module from the local host to the bucket
247
+ logger.info('CemAcpt') { "Uploading #{@run_data[:module_pakage_path]} to #{@run_data[:win_remote_module_path]}..." }
248
+ CemAcpt::Utils::Shell.run_cmd("gcloud storage cp #{@run_data[:module_package_path]} #{@run_data[:win_remote_module_path]}")
249
+ logger.debug('CemAcpt') { 'Successfully uploaded module' }
250
+ end
251
+
252
+ # We have to clean up our gcp bucket after we're done testing. This will limit the duplicated
253
+ # modules existing in the bucket.
254
+ def cleanup_bucket
255
+ if @run_data[:win_remote_module_path]
256
+ logger.info('CemAcpt::TestRunner') { "Cleaning up bucket #{@run_data[:win_remote_module_path]}..." }
257
+ # Cleanup the module from the bucket
258
+ cleanup_cmd = CemAcpt::Utils::Shell.run_cmd("gcloud storage rm #{@run_data[:win_remote_module_path]}", raise_on_error: false)
259
+ logger.debug('CemAcpt::TestRunner') { "Removed module from bucket: #{cleanup_cmd}" } unless cleanup_cmd.nil? || cleanup_cmd.empty?
260
+ else
261
+ logger.info('CemAcpt::TestRunner') { 'No module to clean up in bucket...' }
262
+ end
184
263
  end
185
264
  end
186
265
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'stringio'
5
+
6
+ module CemAcpt
7
+ # Error class for shell commands
8
+ class ShellCommandError < StandardError; end
9
+
10
+ module Utils
11
+ # Generic utilities for running local shell commands
12
+ module Shell
13
+ # Runs a command in a subshell and returns the Process::Status
14
+ # and the string output of the command.
15
+ # @param cmd [String] The command to run
16
+ # @param env [Hash] A hash of environment variables to set
17
+ # @param output [IO] An IO object that implements #:<< to write the output of the
18
+ # command to in real time. Typically this is a Logger object. Defaults to $stdout.
19
+ # If the object responds to #:debug, the command will be logged at the debug level.
20
+ # @param raise_on_fail [Boolean] Whether to raise an error if the command fails
21
+ # @return [String] The string output of the command
22
+ def self.run_cmd(cmd, env = {}, output: $stdout, raise_on_fail: true)
23
+ io_outerr = StringIO.new
24
+ if output.respond_to?(:debug)
25
+ output.debug('CemAcpt::Utils::Shell') { "Running command:\n\t#{cmd}\nWith environment:\n\t#{env}" }
26
+ else
27
+ output << "Running command:\n\t#{cmd}\nWith environment:\n\t#{env}\n"
28
+ end
29
+ val = Open3.popen2e(env, cmd) do |stdin, outerr, wait_thr|
30
+ stdin.close
31
+ outerr.sync = true
32
+ output_thread = Thread.new do
33
+ while (line = outerr.readline_nonblock)
34
+ output << line if output
35
+ io_outerr.write(line) unless line.chomp.empty?
36
+ end
37
+ rescue IO::WaitReadable
38
+ begin
39
+ IO.select([outerr])
40
+ retry
41
+ rescue IOError
42
+ # outerr closed, won't retry
43
+ end
44
+ rescue EOFError
45
+ # outerr closed, won't retry
46
+ end
47
+ wait_thr.join
48
+ output_thread.exit
49
+ wait_thr.value
50
+ end
51
+ io_string = io_outerr.string
52
+ raise CemAcpt::ShellCommandError, "Error running command: #{cmd}\n#{io_string}" if raise_on_fail && !val.success?
53
+
54
+ io_string
55
+ end
56
+
57
+ # Mimics the behavior of the `which` command.
58
+ # @param cmd [String] The command to find
59
+ # @return [String] The path to the command
60
+ # @return [nil] If the command is not found
61
+ def self.which(cmd)
62
+ return cmd if File.executable?(cmd) && !File.directory?(cmd)
63
+
64
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
65
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
66
+ exts.each do |ext|
67
+ exe = File.join(path, "#{cmd}#{ext}")
68
+ return exe if File.executable?(exe) && !File.directory?(exe)
69
+ end
70
+ end
71
+ nil
72
+ end
73
+
74
+ # IO monkey patch for non-blocking readline
75
+ class ::IO
76
+ def readline_nonblock
77
+ rlnb = []
78
+ rnlb << read_nonblock(1) while rlnb[-1] != "\n"
79
+ rlnb.join
80
+ rescue IO::WaitReadable => blocking
81
+ raise blocking if rlnb.empty?
82
+
83
+ rlnb.join
84
+ rescue EOFError
85
+ rlnb.join
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'open3'
3
+ require_relative 'shell'
4
4
  require_relative '../logging'
5
5
 
6
6
  module CemAcpt
@@ -31,10 +31,8 @@ module CemAcpt
31
31
  def create(key_name, **options)
32
32
  delete(key_name) # Delete existing keys with same name
33
33
  cmd = new_keygen_cmd(key_name, **options)
34
- logger.debug("Creating SSH key with command: #{cmd}")
35
- _stdout, stderr, status = Open3.capture3(cmd)
36
- raise "Failed to create SSH key! #{stderr}" unless status.success?
37
-
34
+ logger.debug('CemAcpt::Utils::SSH::Keygen') { "Creating SSH key with command: #{cmd}" }
35
+ CemAcpt::Utils::Shell.run_cmd(cmd, output: logger)
38
36
  key_paths(key_name)
39
37
  end
40
38
 
@@ -42,7 +40,7 @@ module CemAcpt
42
40
  priv_key = key_path(key_name)
43
41
  pub_key = key_path(key_name, public_key: true)
44
42
  if ::File.file?(priv_key)
45
- logger.debug("Deleting private key: #{priv_key}")
43
+ logger.debug('CemAcpt::Utils::SSH::Keygen') { "Deleting private key: #{priv_key}" }
46
44
  ::File.delete(priv_key)
47
45
  end
48
46
  if ::File.file?(pub_key)
@@ -53,20 +51,11 @@ module CemAcpt
53
51
 
54
52
  private
55
53
 
56
- FIND_BIN_PATH_COMMANDS = [
57
- "#{ENV['SHELL']} -c 'command -v ssh-keygen'",
58
- "#{ENV['SHELL']} -c 'which ssh-keygen'",
59
- ].freeze
60
-
61
- def find_bin_path(find_cmd = FIND_BIN_PATH_COMMANDS.first)
62
- bin_path, stderr, status = Open3.capture3(find_cmd)
63
- raise "Cannot find ssh-keygen with command #{find_cmd}: #{stderr}" unless status.success?
64
-
65
- bin_path.chomp
66
- rescue StandardError => e
67
- return find_bin_path(FIND_BIN_PATH_COMMANDS.last) unless FIND_BIN_PATH_COMMANDS.last == find_cmd
54
+ def find_bin_path
55
+ bin_path = CemAcpt::Utils::Shell.which('ssh-keygen')
56
+ raise 'Cannot find command ssh-keygen in PATH, make sure it is installed' if bin_path.nil?
68
57
 
69
- raise e
58
+ bin_path
70
59
  end
71
60
 
72
61
  def new_keygen_cmd(key_name, **options)
@@ -100,12 +89,10 @@ module CemAcpt
100
89
  end
101
90
 
102
91
  def self.ssh_keygen
103
- bin_path = `#{ENV['SHELL']} -c 'command -v ssh-keygen'`.chomp
104
- raise 'Cannot find ssh-keygen! Install it and verify PATH' unless bin_path
92
+ bin_path = CemAcpt::Utils::Shell.which('ssh-keygen')
93
+ raise 'Cannot find command ssh-keygen in PATH, make sure it is installed' if bin_path.nil?
105
94
 
106
95
  bin_path
107
- rescue StandardError => e
108
- raise "Cannot find ssh-keygen! Install it and verify PATH. Orignal error: #{e}"
109
96
  end
110
97
 
111
98
  def self.default_keydir
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent-ruby'
4
-
5
3
  module CemAcpt
6
4
  module Utils
7
5
  # Terminal-related utilities
8
6
  module Terminal
9
7
  def self.keep_terminal_alive
10
- executor = Concurrent::SingleThreadExecutor.new
11
- executor.post do
8
+ Thread.new do
12
9
  loop do
13
10
  $stdout.print(".\r")
14
11
  sleep(1)
@@ -20,7 +17,6 @@ module CemAcpt
20
17
  sleep(1)
21
18
  end
22
19
  end
23
- executor
24
20
  end
25
21
  end
26
22
  end