cem_acpt 0.2.6-universal-java-17

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/CODEOWNERS +1 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +93 -0
  7. data/README.md +150 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/cem_acpt.gemspec +39 -0
  12. data/exe/cem_acpt +84 -0
  13. data/lib/cem_acpt/bootstrap/bootstrapper.rb +206 -0
  14. data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +129 -0
  15. data/lib/cem_acpt/bootstrap/operating_system.rb +17 -0
  16. data/lib/cem_acpt/bootstrap.rb +12 -0
  17. data/lib/cem_acpt/context.rb +153 -0
  18. data/lib/cem_acpt/core_extensions.rb +108 -0
  19. data/lib/cem_acpt/image_name_builder.rb +104 -0
  20. data/lib/cem_acpt/logging.rb +351 -0
  21. data/lib/cem_acpt/platform/base/cmd.rb +71 -0
  22. data/lib/cem_acpt/platform/base.rb +78 -0
  23. data/lib/cem_acpt/platform/gcp/cmd.rb +345 -0
  24. data/lib/cem_acpt/platform/gcp/compute.rb +332 -0
  25. data/lib/cem_acpt/platform/gcp.rb +85 -0
  26. data/lib/cem_acpt/platform/vmpooler.rb +24 -0
  27. data/lib/cem_acpt/platform.rb +103 -0
  28. data/lib/cem_acpt/puppet_helpers.rb +39 -0
  29. data/lib/cem_acpt/rspec_utils.rb +242 -0
  30. data/lib/cem_acpt/shared_objects.rb +537 -0
  31. data/lib/cem_acpt/spec_helper_acceptance.rb +184 -0
  32. data/lib/cem_acpt/test_data.rb +146 -0
  33. data/lib/cem_acpt/test_runner/run_handler.rb +187 -0
  34. data/lib/cem_acpt/test_runner/runner.rb +210 -0
  35. data/lib/cem_acpt/test_runner/runner_result.rb +103 -0
  36. data/lib/cem_acpt/test_runner.rb +10 -0
  37. data/lib/cem_acpt/utils.rb +144 -0
  38. data/lib/cem_acpt/version.rb +5 -0
  39. data/lib/cem_acpt.rb +34 -0
  40. data/sample_config.yaml +58 -0
  41. metadata +218 -0
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent-ruby'
4
+ require 'English'
5
+ require_relative '../logging'
6
+ require_relative '../rspec_utils'
7
+ require_relative 'runner_result'
8
+
9
+ module CemAcpt
10
+ module TestRunner
11
+ class RunnerError < StandardError; end
12
+ # Error used to wrap fatal errors raised in Runner steps
13
+ class RunnerStepError < StandardError
14
+ attr_reader :step
15
+
16
+ def initialize(step, err)
17
+ @step = step
18
+ super err
19
+ set_backtrace err.backtrace if err.respond_to?(:backtrace)
20
+ end
21
+ end
22
+ class RunnerProvisionError < RunnerStepError; end
23
+
24
+ # Runner is a class that runs a single acceptance test suite on a single node.
25
+ # It is responsible for managing the lifecycle of the test suite and
26
+ # reporting the results back to the main thread. Runner objects are created
27
+ # by the RunHandler and then, when started, execute their logic in a thread.
28
+ class Runner
29
+ include CemAcpt::LoggingAsync
30
+
31
+ attr_reader :node, :node_exists, :run_result
32
+
33
+ # @param node [String] the name of the node to run the acceptance test suite on
34
+ # @param ctx [CemAcpt::RunnerCtx] a cem_acpt Ctx (context) object
35
+ # @param module_pkg_path [Concurrent::IVar] the path to the module package
36
+ def initialize(node, context, platform)
37
+ @node = node
38
+ @context = context
39
+ @platform = platform
40
+ @debug_mode = @context.config.debug_mode?
41
+ @node_inventory = @context.node_inventory
42
+ @module_pkg_path = @context.module_package_path
43
+ @node_exists = false
44
+ @run_result = CemAcpt::TestRunner::RunnerResult.new(@node, debug: @debug_mode)
45
+ @completed_steps = []
46
+ validate!
47
+ end
48
+
49
+ def run_step(step_sym)
50
+ send(step_sym)
51
+ @completed_steps << step_sym
52
+ rescue StandardError => e
53
+ err = CemAcpt::TestRunner::RunnerStepError.new(step_sym, e)
54
+ step_error_logging(err)
55
+ @run_result.from_error(err)
56
+ destroy unless step_sym == :destroy
57
+ end
58
+
59
+ # Executes test suite steps
60
+ def start
61
+ async_info("Starting test suite for #{@node.node_name}", log_prefix('RUNNER'))
62
+ run_step(:provision)
63
+ run_step(:bootstrap)
64
+ run_step(:run_tests)
65
+ run_step(:destroy)
66
+ true
67
+ rescue StandardError => e
68
+ step_error_logging(e)
69
+ @run_result.from_error(e)
70
+ destroy
71
+ end
72
+
73
+ # Checks for failures in the test results.
74
+ # @param result [Hash] the test result to check
75
+ # @return [Boolean] whether or not there are test failures in result
76
+ def test_failures?
77
+ @run_result.result_errors? || @run_result.result_failures?
78
+ end
79
+
80
+ private
81
+
82
+ def step_error_logging(err)
83
+ prefix = err.respond_to?(:step) ? log_prefix(err.step.capitalize) : log_prefix('RUNNER')
84
+ fatal_msg = ["runner failed: #{err.message}"]
85
+ async_fatal(fatal_msg, prefix)
86
+ async_debug("Completed steps: #{@completed_steps}", prefix)
87
+ async_debug("Failed runner backtrace:\n#{err.backtrace.join("\n")}", prefix)
88
+ async_debug("Failed runner test data: #{@node.test_data}", prefix)
89
+ end
90
+
91
+ def log_prefix(prefix)
92
+ "#{prefix}: #{@node.test_data[:test_name]}:"
93
+ end
94
+
95
+ # Provisions the node for the acceptance test suite.
96
+ def provision
97
+ async_info("Provisioning #{@node.node_name}...", log_prefix('PROVISION'))
98
+ start_time = Time.now
99
+ @node.provision
100
+ @node_exists = true
101
+ max_retries = 60 # equals 300 seconds because we check every five seconds
102
+ until @node.ready?
103
+ if max_retries <= 0
104
+ async_fatal("Node #{@node.node_name} failed to provision", log_prefix('PROVISION'))
105
+ raise CemAcpt::TestRunner::RunnerProvisionError, "Provisioning timed out for node #{@node.node_name}"
106
+ end
107
+
108
+ async_info("Waiting for #{@node.node_name} to be ready for remote connections...", log_prefix('PROVISION'))
109
+ max_retries -= 1
110
+ sleep(5)
111
+ end
112
+ async_info("Node #{@node.node_name} is ready...", log_prefix('PROVISION'))
113
+ node_desc = {
114
+ test_data: @node.test_data,
115
+ platform: @platform,
116
+ local_port: @node.local_port,
117
+ }.merge(@node.node)
118
+ @node_inventory.add(@node.node_name, node_desc)
119
+ @node_inventory.save
120
+ async_info("Node #{@node.node_name} provisioned in #{Time.now - start_time} seconds", log_prefix('PROVISION'))
121
+ end
122
+
123
+ # Bootstraps the node for the acceptance test suite. Currently, this
124
+ # just uploads and installs the module package.
125
+ def bootstrap
126
+ async_info("Bootstrapping #{@node.node_name}...", log_prefix('BOOTSTRAP'))
127
+ until File.exist?(@module_pkg_path)
128
+ async_debug("Waiting for module package #{@module_pkg_path} to exist...", log_prefix('BOOTSTRAP'))
129
+ sleep(1)
130
+ end
131
+ async_info("Installing module package #{@module_pkg_path}...", log_prefix('BOOTSTRAP'))
132
+ @node.install_puppet_module_package(@module_pkg_path)
133
+ end
134
+
135
+ # Runs the acceptance test suite via rspec.
136
+ def run_tests
137
+ attempts = 0
138
+ until File.exist?(@node_inventory.save_file_path)
139
+ raise 'Node inventory file not found' if (attempts += 1) > 3
140
+
141
+ sleep(1)
142
+ end
143
+ async_info("Running test #{@node.test_data[:test_name]} on node #{@node.node_name}...", log_prefix('RSPEC'))
144
+ @node.run_tests do |cmd_env|
145
+ cmd_opts = rspec_opts
146
+ cmd_opts[:env].merge!(cmd_env) if cmd_env
147
+ # Documentation format gets logged in real time, JSON file is read after the fact
148
+ begin
149
+ @rspec_cmd = CemAcpt::RSpecUtils::Command.new(cmd_opts)
150
+ @rspec_cmd.execute(log_prefix: log_prefix('RSPEC'))
151
+ @run_result.from_json_file(cmd_opts[:format][:json])
152
+ rescue Errno::EIO => e
153
+ async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{$ERROR_INFO}", log_prefix('RSPEC'))
154
+ @run_result.from_error(e)
155
+ rescue StandardError => e
156
+ async_error("failed to run rspec: #{@node.test_data[:test_name]}: #{e.message}", log_prefix('RSPEC'))
157
+ async_debug("Backtrace:\n#{e.backtrace}", log_prefix('RSPEC'))
158
+ @run_result.from_error(e)
159
+ end
160
+ end
161
+ async_info("Tests completed with exit code: #{@run_result.exit_status}", log_prefix('RSPEC'))
162
+ end
163
+
164
+ # Destroys the node for the acceptance test suite.
165
+ def destroy
166
+ kill_spec_pty_if_exists
167
+ if @context.config.get('no_destroy_nodes')
168
+ async_info("Not destroying node #{@node.node_name} because 'no_destroy_nodes' is set to true",
169
+ log_prefix('DESTROY'))
170
+ else
171
+ async_info("Destroying #{@node.node_name}...", log_prefix('DESTROY'))
172
+ @node.destroy
173
+ @node_exists = false
174
+ async_info("Node #{@node.node_name} destroyed successfully", log_prefix('DESTROY'))
175
+ end
176
+ end
177
+
178
+ def kill_spec_pty_if_exists
179
+ @rspec_cmd&.kill_pty
180
+ end
181
+
182
+ # Validates the runner configuration.
183
+ def validate!
184
+ raise 'No node provided' unless @node
185
+ raise 'Node does not have config' if @node.config.nil? || @node.config.empty?
186
+ raise 'Node does not have test data' if @node.test_data.nil? || @node.test_data.empty?
187
+ raise 'No node inventory provided' unless @node_inventory
188
+ end
189
+
190
+ # Options used with RSpec
191
+ def rspec_opts
192
+ opts = {
193
+ test_path: @node.test_data[:test_file],
194
+ use_bundler: false,
195
+ bundle_install: false,
196
+ format: {
197
+ json: "results_#{@node.test_data[:test_name]}.json",
198
+ },
199
+ debug: (@debug_mode && @context.config.get('verbose')),
200
+ quiet: @context.config.get('quiet'),
201
+ env: {
202
+ 'TARGET_HOST' => @node.node_name,
203
+ }
204
+ }
205
+ opts[:format][:documentation] = nil unless @context.config.get('verbose')
206
+ opts
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ module TestRunner
5
+ require 'json'
6
+
7
+ # Error thrown when there are problems parsing the JSON output from Rspec
8
+ class RspecOutputJSONParserError < StandardError
9
+ def initialize(err = nil)
10
+ super err
11
+ set_backtrace err.backtrace unless err.nil?
12
+ end
13
+ end
14
+
15
+ # Class to process the results of a Runner
16
+ class RunnerResult
17
+ attr_reader :run_result
18
+
19
+ def initialize(node, debug: false)
20
+ @node = node
21
+ @run_result = {}
22
+ @debug = debug
23
+ end
24
+
25
+ def to_h
26
+ @run_result
27
+ end
28
+
29
+ def debug?
30
+ @debug
31
+ end
32
+
33
+ def exit_status
34
+ if run_result.nil?
35
+ 99
36
+ elsif run_result.empty?
37
+ 66
38
+ elsif result_errors? || result_failures?
39
+ 1
40
+ else
41
+ 0
42
+ end
43
+ end
44
+
45
+ def result_array
46
+ [run_result, exit_status]
47
+ end
48
+
49
+ def from_json_file(file_path)
50
+ res = JSON.parse(File.read(file_path))
51
+ new_run_result(res)
52
+ rescue StandardError => e
53
+ from_error(RspecOutputJSONParserError.new(e))
54
+ end
55
+
56
+ def from_error(err)
57
+ label = err.class.to_s.start_with?('Errno::') ? 'system_error' : 'standard_error'
58
+ res = {
59
+ label => {
60
+ 'message' => err.message,
61
+ 'error_class' => err.class.to_s,
62
+ 'backtrace' => err.backtrace,
63
+ 'cause' => err.cause,
64
+ 'full_message' => err.full_message,
65
+ }
66
+ }
67
+ res[label]['errno'] = err.errno if label == 'system_error'
68
+ new_run_result(res)
69
+ end
70
+
71
+ def result_errors?
72
+ run_result.keys.any? { |k| k.end_with?('error') }
73
+ end
74
+
75
+ def result_failures?
76
+ if run_result['summary']
77
+ run_result['summary']['failure_count'].positive? ||
78
+ run_result['summary']['errors_outside_of_examples_count'].positive?
79
+ else
80
+ true
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def new_run_result(results)
87
+ @run_result = debug? ? with_debug_data(results) : results
88
+ end
89
+
90
+ def with_debug_data(results)
91
+ results.merge(
92
+ {
93
+ 'debug' => {
94
+ 'node' => @node,
95
+ 'config' => @config,
96
+ 'test_data' => @test_data,
97
+ }
98
+ }
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ # Namespace for all Runner-related classes and modules
5
+ module TestRunner
6
+ require_relative 'test_runner/run_handler'
7
+ require_relative 'test_runner/runner'
8
+ require_relative 'test_runner/runner_result'
9
+ end
10
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'securerandom'
5
+
6
+ module CemAcpt
7
+ # Utility methods and modules for CemAcpt.
8
+ module Utils
9
+ def self.os
10
+ case RbConfig::CONFIG['host_os']
11
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
12
+ :windows
13
+ else
14
+ :nix
15
+ end
16
+ end
17
+
18
+ # File-related utilities
19
+ module File
20
+ def self.set_permissions(permission, *file_paths)
21
+ file_paths.map { |p| ::File.chmod(permission, p) }
22
+ end
23
+ end
24
+
25
+ # Puppet-related utilities
26
+ module Puppet
27
+ DEFAULT_PUPPET_PATH = {
28
+ nix: '/opt/puppetlabs/bin/puppet',
29
+ windows: 'C:/Program Files/Puppet Labs/Puppet/bin/puppet.bat',
30
+ }.freeze
31
+
32
+ # Finds and returns the Puppet executable
33
+ def self.puppet_executable
34
+ this_os = CemAcpt::Utils.os
35
+ if ::File.file?(DEFAULT_PUPPET_PATH[this_os]) && ::File.executable?(DEFAULT_PUPPET_PATH[this_os])
36
+ return DEFAULT_PUPPET_PATH[this_os]
37
+ end
38
+
39
+ file_name = 'puppet'
40
+ if this_os == :windows
41
+ exts = ENV['PATHEXT'] ? ".{#{ENV['PATHEXT'].tr(';', ',').tr('.', '').downcase}}" : '.{exe,com,bat}'
42
+ file_name = "#{file_name}#{exts}"
43
+ end
44
+ ENV['PATH'].split(::File::PATH_SEPARATOR).each do |path|
45
+ if ::File.file?(::File.join(path, file_name)) && ::File.executable?(::File.join(path, file_name))
46
+ return ::File.join(path, file_name)
47
+ end
48
+ end
49
+ raise 'Could not find Puppet executable! Is Puppet installed?'
50
+ end
51
+
52
+ # Builds a Puppet module package.
53
+ # @param module_dir [String] Path to the module directory. If target_dir
54
+ # is specified as a relative path, it will be relative to the module dir.
55
+ # @param target_dir [String] Path to the target directory where the package
56
+ # will be built. This defaults to the relative path 'pkg/'.
57
+ # @param should_log [Boolean] Whether or not to log the build process.
58
+ # @return [String] Path to the built package.
59
+ def self.build_module_package(module_dir, target_dir = nil, should_log: false)
60
+ require 'puppet/modulebuilder'
61
+ require 'fileutils'
62
+
63
+ builder_logger = should_log ? logger : nil
64
+ builder = ::Puppet::Modulebuilder::Builder.new(::File.expand_path(module_dir), target_dir, builder_logger)
65
+
66
+ # Validates module metadata by raising exception if invalid
67
+ _metadata = builder.metadata
68
+
69
+ # Builds the module package
70
+ builder.build
71
+ end
72
+ end
73
+
74
+ # Node-related utilities
75
+ module Node
76
+ def self.random_instance_name(prefix: 'cem-acpt-', length: 24)
77
+ rand_length = length - prefix.length
78
+ "#{prefix}#{::SecureRandom.hex(rand_length)}"
79
+ end
80
+ end
81
+
82
+ # SSH-related utilities
83
+ module SSH
84
+ def self.ssh_keygen
85
+ bin_path = `#{ENV['SHELL']} -c 'command -v ssh-keygen'`.chomp
86
+ raise 'Cannot find ssh-keygen! Install it and verify PATH' unless bin_path
87
+
88
+ bin_path
89
+ rescue StandardError => e
90
+ raise "Cannot find ssh-keygen! Install it and verify PATH. Orignal error: #{e}"
91
+ end
92
+
93
+ def self.default_keydir
94
+ ssh_dir = ::File.join(ENV['HOME'], '.ssh')
95
+ raise "SSH directory at #{ssh_dir} does not exist" unless ::File.directory?(ssh_dir)
96
+
97
+ ssh_dir
98
+ end
99
+
100
+ def self.ephemeral_ssh_key(type: 'rsa', bits: '4096', comment: nil, keydir: default_keydir)
101
+ raise ArgumentError, 'keydir does not exist' unless ::File.directory?(keydir)
102
+
103
+ keyfile = ::File.join(keydir, 'acpt_test_key')
104
+ keygen_cmd = [ssh_keygen, "-t #{type}", "-b #{bits}", "-f #{keyfile}", '-N ""']
105
+ keygen_cmd << "-C \"#{comment}\"" if comment
106
+ _, stderr, status = Open3.capture3(keygen_cmd.join(' '))
107
+ raise "Failed to generate ephemeral SSH key: #{stderr}" unless status.success?
108
+
109
+ [keyfile, "#{keyfile}.pub"]
110
+ end
111
+
112
+ def self.acpt_known_hosts(keydir: default_keydir, file_name: 'acpt_known_hosts', overwrite: true)
113
+ kh_file = ::File.join(keydir, file_name)
114
+ ::File.open(kh_file, 'w') { |f| f.write("\n") } unless ::File.exist?(kh_file) && !overwrite
115
+ kh_file
116
+ end
117
+
118
+ def self.set_ssh_file_permissions(priv_key, pub_key, known_hosts)
119
+ CemAcpt::Utils::File.set_permissions(0o600, priv_key, pub_key, known_hosts)
120
+ end
121
+ end
122
+
123
+ # Terminal-related utilities
124
+ module Terminal
125
+ def self.keep_terminal_alive
126
+ require 'concurrent-ruby'
127
+ executor = Concurrent::SingleThreadExecutor.new
128
+ executor.post do
129
+ loop do
130
+ $stdout.print(".\r")
131
+ sleep(1)
132
+ $stdout.print("..\r")
133
+ sleep(1)
134
+ $stdout.print("...\r")
135
+ sleep(1)
136
+ $stdout.print(" \r")
137
+ sleep(1)
138
+ end
139
+ end
140
+ executor
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ VERSION = '0.2.6'
5
+ end
data/lib/cem_acpt.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ require_relative 'cem_acpt/logging'
5
+ require_relative 'cem_acpt/version'
6
+ require_relative 'cem_acpt/spec_helper_acceptance'
7
+
8
+ class << self
9
+ include CemAcpt::Logging
10
+ end
11
+
12
+ def self.run(params)
13
+ require_relative 'cem_acpt/context'
14
+
15
+ log_level = params[:log_level]
16
+ log_formatter = params[:log_format] ? new_log_formatter(params[:log_format]) : nil
17
+ logdevs = [$stdout]
18
+ if params[:log_file] && params[:quiet]
19
+ logdevs = [params[:log_file]]
20
+ elsif params[:log_file] && !params[:quiet]
21
+ logdevs << params[:log_file]
22
+ end
23
+ if (params[:CI] || ENV['CI'] || ENV['GITHUB_ACTION']) && !logdevs.include?($stdout)
24
+ logdevs << $stdout
25
+ end
26
+ new_logger(
27
+ *logdevs,
28
+ level: log_level,
29
+ formatter: log_formatter,
30
+ )
31
+ exit_code = CemAcpt::Context.with(params, &:run)
32
+ exit exit_code
33
+ end
34
+ end
@@ -0,0 +1,58 @@
1
+ test_data:
2
+ for_each:
3
+ collection:
4
+ - puppet6
5
+ - puppet7
6
+ vars:
7
+ test_static_var: 'static_var_value'
8
+ name_pattern_vars: !ruby/regexp '/^(?<framework>[a-z]+)_(?<image_fam>[a-z0-9-]+)_(?<firewall>[a-z]+)_(?<framework_vars>[-_a-z0-9]+)$/'
9
+ vars_post_processing:
10
+ new_vars:
11
+ - name: 'profile'
12
+ string_split:
13
+ from: 'framework_vars'
14
+ using: '_'
15
+ part: 0
16
+ - name: 'level'
17
+ string_split:
18
+ from: 'framework_vars'
19
+ using: '_'
20
+ part: 1
21
+ delete_vars:
22
+ - 'framework_vars'
23
+
24
+ platform: gcp
25
+
26
+ node_data:
27
+ machine_type: 'e2-small'
28
+ project:
29
+ name: 'some-project'
30
+ zone: 'us-west1-b'
31
+ disk:
32
+ name: 'disk-1'
33
+ size: 20
34
+ type: 'pd-standard'
35
+ network_interface:
36
+ tier: 'STANDARD'
37
+ subnetwork: 'some-subnet'
38
+ metadata:
39
+ ephemeral_ssh_key:
40
+ lifetime: 2
41
+ keydir: '/tmp/acpt_test_keys'
42
+
43
+ image_name_builder:
44
+ character_substitutions:
45
+ - ['_', '-']
46
+ parts:
47
+ - 'cem-acpt'
48
+ - '$image_fam'
49
+ - '$collection'
50
+ - '$firewall'
51
+ join_with: '-'
52
+
53
+ tests:
54
+ - cis_rhel-7_firewalld_server_1
55
+ - cis_rhel-7_iptables_server_1
56
+ - cis_rhel-8_firewalld_server_1
57
+ - cis_rhel-8_firewalld_server_2
58
+ - cis_rhel-8_iptables_server_1