cem_acpt 0.8.8 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +0 -3
  3. data/Gemfile.lock +9 -1
  4. data/README.md +95 -13
  5. data/cem_acpt.gemspec +2 -1
  6. data/lib/cem_acpt/action_result.rb +8 -2
  7. data/lib/cem_acpt/actions.rb +153 -0
  8. data/lib/cem_acpt/bolt/cmd/base.rb +174 -0
  9. data/lib/cem_acpt/bolt/cmd/output.rb +315 -0
  10. data/lib/cem_acpt/bolt/cmd/task.rb +59 -0
  11. data/lib/cem_acpt/bolt/cmd.rb +22 -0
  12. data/lib/cem_acpt/bolt/errors.rb +49 -0
  13. data/lib/cem_acpt/bolt/helpers.rb +52 -0
  14. data/lib/cem_acpt/bolt/inventory.rb +62 -0
  15. data/lib/cem_acpt/bolt/project.rb +38 -0
  16. data/lib/cem_acpt/bolt/summary_results.rb +96 -0
  17. data/lib/cem_acpt/bolt/tasks.rb +181 -0
  18. data/lib/cem_acpt/bolt/tests.rb +415 -0
  19. data/lib/cem_acpt/bolt/yaml_file.rb +74 -0
  20. data/lib/cem_acpt/bolt.rb +142 -0
  21. data/lib/cem_acpt/cli.rb +6 -0
  22. data/lib/cem_acpt/config/base.rb +4 -0
  23. data/lib/cem_acpt/config/cem_acpt.rb +7 -1
  24. data/lib/cem_acpt/core_ext.rb +25 -0
  25. data/lib/cem_acpt/goss/api/action_response.rb +4 -0
  26. data/lib/cem_acpt/goss/api.rb +23 -25
  27. data/lib/cem_acpt/image_builder/provision_commands.rb +43 -0
  28. data/lib/cem_acpt/logging/formatter.rb +3 -3
  29. data/lib/cem_acpt/logging.rb +17 -1
  30. data/lib/cem_acpt/provision/terraform/linux.rb +2 -2
  31. data/lib/cem_acpt/test_data.rb +2 -0
  32. data/lib/cem_acpt/test_runner/log_formatter/base.rb +73 -0
  33. data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +65 -0
  34. data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +54 -0
  35. data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +64 -0
  36. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +17 -30
  37. data/lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb +31 -0
  38. data/lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb +35 -0
  39. data/lib/cem_acpt/test_runner/log_formatter.rb +17 -5
  40. data/lib/cem_acpt/test_runner/test_results.rb +150 -0
  41. data/lib/cem_acpt/test_runner.rb +153 -53
  42. data/lib/cem_acpt/utils/files.rb +189 -0
  43. data/lib/cem_acpt/utils/finalizer_queue.rb +73 -0
  44. data/lib/cem_acpt/utils/shell.rb +13 -4
  45. data/lib/cem_acpt/version.rb +1 -1
  46. data/sample_config.yaml +13 -0
  47. metadata +41 -5
  48. data/lib/cem_acpt/test_runner/log_formatter/error_formatter.rb +0 -33
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'yaml'
6
+ require_relative 'logging'
7
+ require_relative 'bolt/cmd'
8
+ require_relative 'bolt/helpers'
9
+ require_relative 'bolt/inventory'
10
+ require_relative 'bolt/project'
11
+ require_relative 'bolt/summary_results'
12
+ require_relative 'bolt/tasks'
13
+ require_relative 'bolt/tests'
14
+ require_relative 'utils/files'
15
+ require_relative 'utils/puppet'
16
+ require_relative 'utils/shell'
17
+
18
+ module CemAcpt
19
+ # Holds all the Bolt related code
20
+ module Bolt
21
+ # Class for running Bolt tests
22
+ class TestRunner
23
+ include CemAcpt::Logging
24
+ include CemAcpt::Bolt::Helpers
25
+
26
+ attr_reader :hosts, :results, :run_data
27
+
28
+ def initialize(config, run_data: {}, hosts: [])
29
+ @config = config
30
+ @run_data = run_data
31
+ @tests = nil
32
+ @hosts = hosts
33
+ @module_name = File.basename(Dir.pwd).gsub(%r{puppetlabs-}, '')
34
+ @bolt_project_setup = false
35
+ @inventory = CemAcpt::Bolt::Inventory.new(config, hosts, run_data[:private_key])
36
+ @project = CemAcpt::Bolt::Project.new(config, @module_name)
37
+ @results = CemAcpt::Bolt::SummaryResults.new
38
+ end
39
+
40
+ def tests
41
+ @tests ||= CemAcpt::Bolt::Tests::TestList.new(@config, @run_data, @inventory, @project)
42
+ end
43
+
44
+ def setup!
45
+ return if setup?
46
+
47
+ @project.save! unless @project.latest_saved?
48
+ @inventory.save! unless @inventory.latest_saved?
49
+ tests.setup! unless tests.setup?
50
+ end
51
+
52
+ def setup?
53
+ @project.latest_saved? && @inventory.latest_saved? && tests.setup?
54
+ end
55
+
56
+ def teardown!
57
+ logger.debug('CemAcpt::Bolt') { 'Tearing down Bolt project...' }
58
+ @inventory.delete!
59
+ @project.delete!
60
+ end
61
+
62
+ def hosts=(hosts)
63
+ @hosts = hosts
64
+ @inventory.hosts = hosts
65
+ end
66
+
67
+ def run_data=(run_data)
68
+ @run_data = run_data
69
+ @inventory.private_key = run_data[:private_key]
70
+ end
71
+
72
+ def run(threaded: true)
73
+ raise BoltActionError, 'Cannot run Bolt tests, no hosts specified' if hosts.nil? || hosts.empty?
74
+
75
+ logger.info('CemAcpt::Bolt') { "Preparing to run Bolt tests for module #{@module_name}..." }
76
+ if threaded
77
+ run_in_context { |t| run_threaded(t) }
78
+ else
79
+ run_in_context { |t| run_synchronous(t) }
80
+ end
81
+ rescue StandardError => e
82
+ errmsg = "Error running Bolt tests for module #{@module_name}"
83
+ wrapped_error = CemAcpt::Bolt::BoltActionError.new(errmsg, e, "#{self.class.name}#run")
84
+ logger.error('CemAcpt::Bolt') { errmsg }
85
+ logger.debug('CemAcpt::Bolt') { "Backtrace:\n#{e.backtrace.join("\n")}" }
86
+ @results << CemAcpt::Bolt::Tests::TestResult.new(
87
+ 'Bolt test runner error',
88
+ nil,
89
+ nil,
90
+ error: wrapped_error,
91
+ )
92
+ ensure
93
+ @results.finalize!
94
+ @results
95
+ end
96
+
97
+ private
98
+
99
+ def max_threads
100
+ @max_threads ||= @config.get('bolt.max_threads') || 5
101
+ end
102
+
103
+ def run_in_context
104
+ raise 'No block given' unless block_given?
105
+
106
+ setup! unless setup?
107
+ yield @tests
108
+ ensure
109
+ teardown! if setup?
110
+ end
111
+
112
+ def run_threaded(tests_to_run = @tests)
113
+ test_groups = tests_to_run.split_into_groups(max_threads)
114
+ logger.info('CemAcpt::Bolt') do
115
+ "Running #{tests_to_run.length} Bolt test(s) in #{test_groups.length} group(s) against #{hosts.length} host(s)..."
116
+ end
117
+ threads = []
118
+ test_groups.each do |test_group|
119
+ threads << Thread.new do
120
+ test_group.each do |t|
121
+ t.run
122
+ result = t.result
123
+ logger.debug('CemAcpt::Bolt') { "Bolt test result:\n#{result}" }
124
+ @results << result
125
+ end
126
+ end
127
+ end
128
+ threads.each(&:join)
129
+ end
130
+
131
+ def run_synchronous(tests_to_run = @tests)
132
+ logger.info('CemAcpt::Bolt') do
133
+ "Running #{tests_to_run.length} Bolt test(s) against #{hosts.length} host(s)..."
134
+ end
135
+ tests_to_run.to_a.each do |t|
136
+ t.run
137
+ @results << t.result
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
data/lib/cem_acpt/cli.rb CHANGED
@@ -35,6 +35,12 @@ module CemAcpt
35
35
  opts.on('-t', '--tests TESTS', 'Set tests. Example: -t "test1,test2"') do |t|
36
36
  options[:tests] = t.split(',')
37
37
  end
38
+
39
+ opts.on('-B', '--no-bolt-tests', 'Do not run Bolt tests') do
40
+ options[:bolt] ||= {}
41
+ options[:bolt][:tests] ||= {}
42
+ options[:bolt][:tests][:enabled] = false
43
+ end
38
44
  when :cem_acpt_image
39
45
  opts.on('--dry-run', 'Logs the information for the images that would be created. Does not create images.') do
40
46
  options[:dry_run] = true
@@ -47,6 +47,10 @@ module CemAcpt
47
47
  load(opts: opts, config_file: config_file)
48
48
  end
49
49
 
50
+ def inspect
51
+ "#<#{self.class}:#{object_id.to_s(16)}>"
52
+ end
53
+
50
54
  # @return [String] The prefix for environment variables
51
55
  def env_var_prefix
52
56
  'CEM_ACPT'
@@ -8,6 +8,7 @@ module CemAcpt
8
8
  class CemAcpt < Base
9
9
  VALID_KEYS = %i[
10
10
  actions
11
+ bolt
11
12
  image_name_builder
12
13
  module_dir
13
14
  node_data
@@ -19,6 +20,11 @@ module CemAcpt
19
20
  def defaults
20
21
  {
21
22
  actions: {},
23
+ bolt: {
24
+ tests: {
25
+ enabled: true,
26
+ },
27
+ },
22
28
  ci_mode: false,
23
29
  config_file: nil,
24
30
  image_name_builder: {
@@ -38,7 +44,7 @@ module CemAcpt
38
44
  quiet: false,
39
45
  test_data: {
40
46
  for_each: {
41
- collection: %w[puppet7],
47
+ collection: %w[puppet7 puppet8],
42
48
  },
43
49
  vars: {},
44
50
  name_pattern_vars: %r{^(?<framework>[a-z]+)_(?<image_fam>[a-z0-9-]+)_(?<firewall>[a-z]+)_(?<framework_vars>[-_a-z0-9]+)$},
@@ -63,4 +63,29 @@ module CemAcpt::CoreExt
63
63
  alias dset dot_store
64
64
  end
65
65
  end
66
+
67
+ # Refines the Array class with some convenience methods.
68
+ module ExtendedArray
69
+ refine Array do
70
+ # Splits the array into the given number of groups.
71
+ # If the array cannot be evenly split, the last group(s)
72
+ # will contain the remainder.
73
+ # Example:
74
+ # [1, 2, 3, 4, 5].split_into_groups(2) #=> [[1, 2, 3], [4, 5]]
75
+ # [1, 2, 3, 4, 5].split_into_groups(3) #=> [[1, 2, 3], [4], [5]]
76
+ def split_into_groups(num_groups)
77
+ group_size = size / num_groups
78
+ leftovers = size % num_groups
79
+
80
+ groups = []
81
+ start = 0
82
+ num_groups.times do |i|
83
+ length = group_size + ((leftovers > 0 && leftovers > i) ? 1 : 0)
84
+ groups << slice(start, length)
85
+ start += length
86
+ end
87
+ groups
88
+ end
89
+ end
90
+ end
66
91
  end
@@ -39,6 +39,10 @@ module CemAcpt
39
39
  status == 200
40
40
  end
41
41
 
42
+ def error?
43
+ !success?
44
+ end
45
+
42
46
  def results
43
47
  @results ||= @body['results'].map { |r| ActionResponseResult.new(r) }
44
48
  end
@@ -11,17 +11,17 @@ module CemAcpt
11
11
  module Goss
12
12
  # Holds methods for interacting with the Goss API running on a test node.
13
13
  module Api
14
+ # The actions that can be run against the Goss API. The key is the action
15
+ # name and the value is the port/endpoint of the action.
16
+ ACTIONS = {
17
+ acpt: '8080/acpt',
18
+ idempotent: '8081/idempotent',
19
+ noop: '8082/noop',
20
+ }.freeze
21
+
14
22
  class << self
15
23
  include CemAcpt::Logging
16
24
 
17
- # The actions that can be run against the Goss API. The key is the action
18
- # name and the value is the port/endpoint of the action.
19
- ACTIONS = {
20
- acpt: '8080/acpt',
21
- idempotent: '8081/idempotent',
22
- noop: '8082/noop',
23
- }.freeze
24
-
25
25
  # Create a URI for the specified action against the specified host.
26
26
  # @param host [String] The host to run the action against. This should be
27
27
  # a public IP address or a DNS-resolvable name.
@@ -32,31 +32,30 @@ module CemAcpt
32
32
  end
33
33
 
34
34
  # Run the specified actions against the specified hosts asynchronously.
35
- # @param hosts [Array<String>] The hosts to run the actions against. Each
35
+ # @param context [Hash] The context for the test run.
36
+ # @option context [Array<String>] :hosts The hosts to run the actions against. Each
36
37
  # host should be a public IP address or a DNS-resolvable name.
37
- # @param results [Queue] The queue to push the results to.
38
- # @param only [Array<Symbol>] The actions to run.
39
- # @param except [Array<Symbol>] The actions to skip.
38
+ # @option context [Queue] :results The queue to push the results to.
39
+ # @option [Array<String>] :actions The actions to run.
40
40
  # @return [Queue] The queue of results.
41
- def run_actions_async(hosts, results: Queue.new, only: [], except: [])
41
+ def run_actions_async(context = {})
42
+ actions = context[:actions] || []
43
+ hosts = context[:hosts] || []
44
+ results = context[:results] || Queue.new
42
45
  raise ArgumentError, 'hosts must be an Array' unless hosts.is_a?(Array)
43
46
  raise ArgumentError, 'results must be a Queue-like object implementing #<<' unless results.respond_to?(:<<)
44
- raise ArgumentError, 'only must be an Array' unless except.is_a?(Array)
45
- raise ArgumentError, 'except must be an Array' unless except.is_a?(Array)
46
- logger.info('CemAcpt::Goss::Api') { "Running test actions against #{hosts.size} host(s)..." }
47
- only.map!(&:to_sym)
48
- except.map!(&:to_sym)
49
- only_specified = !only.empty?
50
- except_specified = !except.empty?
47
+ raise ArgumentError, 'actions must be an Array' unless actions.is_a?(Array)
48
+
49
+ actions.map!(&:to_sym)
50
+ logger.info('CemAcpt::Goss::Api') do
51
+ "Running test actions #{actions.join(', ')} against #{hosts.size} host(s)..."
52
+ end
51
53
  task = Async do
52
54
  internet = Async::HTTP::Internet.new
53
55
  barrier = Async::Barrier.new
54
56
  barrier.async do
55
57
  hosts.each do |host|
56
- ACTIONS.each_key do |action|
57
- next if only_specified && !only.include?(action)
58
- next if except_specified && except.include?(action)
59
-
58
+ actions.each do |action|
60
59
  results << run_action(internet, host, action)
61
60
  end
62
61
  end
@@ -64,7 +63,6 @@ module CemAcpt
64
63
  barrier.wait
65
64
  ensure
66
65
  internet&.close
67
- results.close if results.respond_to?(:close)
68
66
  end
69
67
  task.wait
70
68
  logger.info('CemAcpt::Goss::Api') { 'Finished running test actions, returning results...' }
@@ -74,6 +74,48 @@ module CemAcpt
74
74
  alias to_a provision_commands
75
75
  end
76
76
 
77
+ class DebianFamily
78
+ def initialize(config, image_name, base_image, os_major_version, puppet_version)
79
+ @config = config
80
+ @image_name = image_name
81
+ @base_image = base_image
82
+ @os_major_version = os_major_version
83
+ @puppet_version = puppet_version
84
+ end
85
+
86
+ def default_provision_commands
87
+ [install_curl_command, enable_puppet_repository_command, upgrade_packages_command, install_puppet_agent_command]
88
+ end
89
+
90
+ def provision_commands
91
+ commands_from_config = @config.get("images.#{@image_name}.provision_commands") || []
92
+ (default_provision_commands + commands_from_config).compact
93
+ end
94
+ alias to_a provision_commands
95
+
96
+ private
97
+
98
+ def upgrade_packages_command
99
+ 'sudo apt-get update && sudo apt-get upgrade -y'
100
+ end
101
+
102
+ def puppet_platform_repository_url
103
+ "https://apt.puppet.com/puppet#{@puppet_version}-release-focal.deb"
104
+ end
105
+
106
+ def enable_puppet_repository_command
107
+ "wget #{puppet_platform_repository_url} && sudo dpkg -i puppet#{@puppet_version}-release-focal.deb"
108
+ end
109
+
110
+ def install_puppet_agent_command
111
+ 'sudo apt-get install -y puppet-agent'
112
+ end
113
+
114
+ # The latest Google-provided Ubuntu images do not have curl installed by default
115
+ def install_curl_command
116
+ 'sudo apt-get install -y curl'
117
+ end
118
+ end
77
119
  class << self
78
120
  # Map of OS to class that holds the provision commands for that OS
79
121
  OS_CLASS_MAP = {
@@ -82,6 +124,7 @@ module CemAcpt
82
124
  'alma' => 'EnterpriseLinuxFamily',
83
125
  'oel' => 'EnterpriseLinuxFamily',
84
126
  'rocky' => 'EnterpriseLinuxFamily',
127
+ 'ubuntu' => 'DebianFamily',
85
128
  'windows' => 'WindowsFamily',
86
129
  }.freeze
87
130
 
@@ -18,7 +18,7 @@ module CemAcpt
18
18
  def all
19
19
  @all ||= [FileFormatter.new, JSONFormatter.new, TextFormatter.new, GithubActionFormatter.new]
20
20
  end
21
- end
21
+ end
22
22
 
23
23
  class FileFormatter
24
24
  attr_reader :log_format
@@ -26,10 +26,10 @@ module CemAcpt
26
26
  def initialize
27
27
  @log_format = :file
28
28
  end
29
-
29
+
30
30
  def get
31
31
  proc do |severity, datetime, progname, msg|
32
- format(severity, datetime, progname, msg)
32
+ self.format(severity, datetime, progname, msg)
33
33
  end
34
34
  end
35
35
 
@@ -73,7 +73,7 @@ module CemAcpt
73
73
 
74
74
  def initialize(logdev, shift_age = 0, shift_size = 1_048_576, **kwargs)
75
75
  @config_hash = { logdev: logdev, shift_age: shift_age, shift_size: shift_size, **kwargs }
76
- @ci_mode = (kwargs[:formatter].downcase.to_sym == :github_action || !ENV['GITHUB_ACTIONS'].nil? || !ENV['CI'].nil?)
76
+ @ci_mode = (kwargs[:formatter]&.downcase&.to_sym == :github_action || !ENV['GITHUB_ACTIONS'].nil? || !ENV['CI'].nil?)
77
77
  kwargs[:formatter] = CemAcpt::Logging::Formatter.for(kwargs[:formatter]) if kwargs[:formatter]
78
78
  super(logdev, shift_age, shift_size, **kwargs)
79
79
  @raw_logdev = logdev # Used for logging from trap context
@@ -93,6 +93,10 @@ module CemAcpt
93
93
  debug(progname, &block)
94
94
  end
95
95
 
96
+ def verbose?
97
+ @verbose
98
+ end
99
+
96
100
  def debug(progname = nil, &block)
97
101
  severity = 'debug'
98
102
  return log_trap_context(severity, progname, &block) if log_trap_context?(severity)
@@ -243,6 +247,10 @@ module CemAcpt
243
247
  @logger.level = LEVEL_MAP[level.downcase]
244
248
  end
245
249
 
250
+ def verbose?
251
+ logger.is_a?(CemAcpt::Logging::MultiLogger) ? logger.verbose?.all? : logger.verbose?
252
+ end
253
+
246
254
  # Shows the current log format style if set, or the default if not.
247
255
  # @return [Symbol] the current log format style
248
256
  def current_log_format
@@ -332,6 +340,10 @@ module CemAcpt
332
340
  CemAcpt::Logging.new_log_level(level)
333
341
  end
334
342
 
343
+ def verbose?
344
+ CemAcpt::Logging.verbose?
345
+ end
346
+
335
347
  def current_log_format
336
348
  CemAcpt::Logging.current_log_format
337
349
  end
@@ -369,6 +381,10 @@ module CemAcpt
369
381
  CemAcpt::Logging.new_log_level(level)
370
382
  end
371
383
 
384
+ def verbose?
385
+ CemAcpt::Logging.verbose?
386
+ end
387
+
372
388
  def current_log_format
373
389
  CemAcpt::Logging.current_log_format
374
390
  end
@@ -7,11 +7,11 @@ module CemAcpt
7
7
  # Class provides methods for gathering provision data for Linux nodes
8
8
  class Linux < OsData
9
9
  def self.valid_names
10
- %w[centos rhel oel alma rocky]
10
+ %w[centos rhel oel alma rocky ubuntu]
11
11
  end
12
12
 
13
13
  def self.valid_versions
14
- %w[7 8 9]
14
+ %w[7 8 9 2004]
15
15
  end
16
16
 
17
17
  def systemd_files
@@ -45,6 +45,7 @@ module CemAcpt
45
45
  raise "Goss file not found for test #{test_name}" unless File.exist?(goss_file)
46
46
  raise "Puppet manifest not found for test #{test_name}" unless File.exist?(puppet_manifest)
47
47
 
48
+ bolt_test = File.expand_path(File.join(test_dir, 'bolt.yaml'))
48
49
  logger.debug('CemAcpt::TestData') { "Complete test directory found for test #{test_name}: #{test_dir}" }
49
50
  test_data = {
50
51
  test_name: test_name,
@@ -52,6 +53,7 @@ module CemAcpt
52
53
  goss_file: goss_file,
53
54
  puppet_manifest: puppet_manifest,
54
55
  }
56
+ test_data[:bolt_test] = bolt_test if File.exist?(bolt_test)
55
57
 
56
58
  process_for_each(test_data).each do |test_data_i|
57
59
  process_static_vars(test_data_i)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ module TestRunner
5
+ module LogFormatter
6
+ # Base class for log formatters
7
+ class Base
8
+ INDENT = ' '
9
+
10
+ attr_accessor :subject
11
+
12
+ def initialize(subject = nil)
13
+ @subject = subject
14
+ end
15
+
16
+ def inspect
17
+ to_s
18
+ end
19
+
20
+ def to_s
21
+ "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
22
+ end
23
+
24
+ def summary(response = nil)
25
+ register_response(response)
26
+ end
27
+
28
+ def results(response = nil)
29
+ register_response(response)
30
+ end
31
+
32
+ def host_name(response = nil)
33
+ register_response(response)
34
+ end
35
+
36
+ def test_name(response = nil)
37
+ register_response(response)
38
+ end
39
+
40
+ private
41
+
42
+ def register_response(response = nil)
43
+ raise ArgumentError, 'either subject or response must be provided' if @subject.nil? && response.nil?
44
+
45
+ @response = response
46
+ end
47
+
48
+ def log_subject
49
+ @response || @subject
50
+ end
51
+
52
+ def normalize_whitespace(str)
53
+ raise ArgumentError, 'str must be a String' unless str.is_a?(String)
54
+
55
+ str.gsub(%r{(\n|\r|\t)}, ' ').gsub(%r{\s{2,}}, ' ').strip
56
+ end
57
+
58
+ def success_str(success)
59
+ success ? 'passed' : 'failed'
60
+ end
61
+
62
+ def name_from_ip(ip)
63
+ raise 'class does not provide instance_names_ips instance variable' if @instance_names_ips.nil?
64
+
65
+ @instance_names_ips.each do |name, val|
66
+ return name if val['ip'] == ip
67
+ end
68
+ ip
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module CemAcpt
6
+ module TestRunner
7
+ module LogFormatter
8
+ class BoltErrorFormatter < Base
9
+ def initialize(subject: nil)
10
+ super(subject)
11
+ end
12
+
13
+ def summary(response = nil)
14
+ super(response)
15
+ "Error: #{log_subject.summary}"
16
+ end
17
+
18
+ def results(response = nil)
19
+ super(response)
20
+ log_subject.results.map do |res|
21
+ format_failed_result(res)
22
+ end
23
+ end
24
+
25
+ def host_name(response = nil)
26
+ super(response)
27
+ "Error: #{log_subject.error.class.name}"
28
+ end
29
+
30
+ def test_name(response = nil)
31
+ super(response)
32
+ "Error: #{log_subject.error.class.name}"
33
+ end
34
+
35
+ private
36
+
37
+ def format_failed_result(res)
38
+ prefix = [
39
+ 'Failed',
40
+ "action #{res.action}",
41
+ "object #{res.object}",
42
+ "target #{res.target}",
43
+ ].join(': ')
44
+ return prefix unless res.error?
45
+
46
+ error_parts = [
47
+ "error type #{res.error.class.name}",
48
+ normalize_whitespace(res.error.to_s),
49
+ ]
50
+ if res.error.respond_to?(:details) && res.error.details
51
+ error_parts << "details #{res.error.details}"
52
+ end
53
+ if res.error.respond_to?(:original_error) && res.error.original_error
54
+ error_parts << "original error #{res.error.original_error.class.name}"
55
+ error_parts << "original message #{res.error.original_error.message}"
56
+ end
57
+ if res.error.respond_to?(:backtrace) && !res.error.backtrace.empty?
58
+ error_parts << "backtrace:\n#{res.error.backtrace.join("\n#{INDENT}#{INDENT}")}"
59
+ end
60
+ "#{prefix}\n#{INDENT}#{error_parts.join("\n#{INDENT}")}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module CemAcpt
6
+ module TestRunner
7
+ module LogFormatter
8
+ # Formats the results of a Bolt action
9
+ class BoltOutputFormatter < Base
10
+ def initialize(config, instance_names_ips, subject: nil)
11
+ super(subject)
12
+ @config = config
13
+ @instance_names_ips = instance_names_ips
14
+ end
15
+
16
+ def summary(response = nil)
17
+ super(response)
18
+ [
19
+ "SUMMARY: #{test_name(log_subject)}:",
20
+ normalize_whitespace(log_subject.summary),
21
+ ].join(' ')
22
+ end
23
+
24
+ def results(response = nil)
25
+ super(response)
26
+ log_subject.results.map do |res|
27
+ prefix = success_str(res.success?).capitalize
28
+ parts = [
29
+ prefix,
30
+ "action #{res.action}",
31
+ "host(s) #{host_name(log_subject)}",
32
+ "object #{res.object}",
33
+ "status #{res.status}",
34
+ ]
35
+ parts << res.error if res.error?
36
+ parts.join(': ')
37
+ end
38
+ end
39
+
40
+ def host_name(response = nil)
41
+ super(response)
42
+ log_subject.items.map { |i| name_from_ip(i.target) }.compact.uniq.join(', ')
43
+ end
44
+
45
+ def test_name(response = nil)
46
+ super(response)
47
+ test_type = (log_subject.items.find { |i| i.action != 'unknown' }&.action || 'unknown')
48
+ test_objects = log_subject.items.map(&:object).uniq.join(', ')
49
+ "Bolt #{test_type} test for: #{test_objects}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end