cem_acpt 0.2.6-universal-java-17

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