cem_acpt 0.11.0 → 0.11.2
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.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.worktreeinclude +1 -0
- data/CLAUDE.md +64 -25
- data/Gemfile.lock +1 -1
- data/README.md +20 -7
- data/docs/ARCHITECTURE.md +1042 -0
- data/docs/rfcs/0000-template.md +54 -0
- data/docs/rfcs/0001-fix-bolt-missing-skip-path.md +105 -0
- data/docs/rfcs/0002-fix-default-character-substitutions.md +119 -0
- data/docs/rfcs/0003-windows-image-builder-template.md +110 -0
- data/docs/rfcs/0004-image-name-truncation-off-by-one.md +108 -0
- data/docs/rfcs/0005-os-dispatch-replace-windows-heuristic.md +117 -0
- data/docs/rfcs/0006-configurable-windows-bucket.md +96 -0
- data/docs/rfcs/0007-logging-quiet-and-typos.md +121 -0
- data/docs/rfcs/0008-namespace-platform-classes.md +110 -0
- data/docs/rfcs/0009-bolt-log-formatter-cleanup.md +111 -0
- data/docs/rfcs/0010-dead-code-cleanup.md +83 -0
- data/docs/rfcs/0011-provisioner-factory-consistency.md +89 -0
- data/docs/rfcs/README.md +34 -0
- data/lib/cem_acpt/cli.rb +10 -1
- data/lib/cem_acpt/config/cem_acpt.rb +4 -1
- data/lib/cem_acpt/image_builder/errors.rb +24 -0
- data/lib/cem_acpt/image_builder/provision_commands.rb +15 -3
- data/lib/cem_acpt/image_builder.rb +29 -2
- data/lib/cem_acpt/image_name_builder.rb +8 -1
- data/lib/cem_acpt/platform/gcp.rb +112 -106
- data/lib/cem_acpt/platform.rb +21 -19
- data/lib/cem_acpt/provision/terraform/linux.rb +1 -1
- data/lib/cem_acpt/provision/terraform/os_data.rb +23 -0
- data/lib/cem_acpt/provision/terraform/windows.rb +7 -1
- data/lib/cem_acpt/provision/terraform.rb +20 -16
- data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +2 -1
- data/lib/cem_acpt/test_runner/log_formatter.rb +0 -1
- data/lib/cem_acpt/test_runner.rb +21 -8
- data/lib/cem_acpt/utils/winrm_runner.rb +4 -3
- data/lib/cem_acpt/utils.rb +0 -12
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +19 -7
- data/lib/terraform/gcp/linux/main.tf +6 -1
- data/lib/terraform/image/gcp/linux/main.tf +8 -1
- data/specifications/CEM-6713.md +165 -0
- data/specifications/CEM-6714.md +271 -0
- data/specifications/CEM-6715.md +133 -0
- data/specifications/CEM-6716.md +160 -0
- data/specifications/CEM-6717.md +239 -0
- data/specifications/CEM-6718.md +120 -0
- data/specifications/CEM-6719.md +173 -0
- metadata +26 -11
- data/.claude/settings.local.json +0 -7
- data/lib/cem_acpt/action_result.rb +0 -91
- data/lib/cem_acpt/puppet_helpers.rb +0 -38
- data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +0 -65
- data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +0 -54
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CemAcpt
|
|
4
|
+
module ImageBuilder
|
|
5
|
+
# Raised when an OS-specific image-builder Terraform template
|
|
6
|
+
# (e.g. `lib/terraform/image/<platform>/windows/main.tf`) is missing
|
|
7
|
+
# but the user has configured at least one image for that OS. The
|
|
8
|
+
# builder catches this before invoking `terraform init` so users get
|
|
9
|
+
# an actionable message instead of Terraform's "no configuration
|
|
10
|
+
# files" output.
|
|
11
|
+
class MissingTemplateError < StandardError
|
|
12
|
+
attr_reader :os_str, :template_path
|
|
13
|
+
|
|
14
|
+
def initialize(os_str, template_path)
|
|
15
|
+
@os_str = os_str
|
|
16
|
+
@template_path = template_path
|
|
17
|
+
super(
|
|
18
|
+
"No #{os_str} image-builder Terraform template ships with this version of cem_acpt " \
|
|
19
|
+
"(expected #{template_path}). Either upgrade cem_acpt or pass --no-#{os_str}."
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -19,7 +19,11 @@ module CemAcpt
|
|
|
19
19
|
|
|
20
20
|
def provision_commands
|
|
21
21
|
commands_from_config = @config.get("images.#{@image_name}.provision_commands") || []
|
|
22
|
-
(
|
|
22
|
+
if @config.get("images.#{@image_name}.skip_default_provision_commands")
|
|
23
|
+
commands_from_config.compact
|
|
24
|
+
else
|
|
25
|
+
(default_provision_commands + commands_from_config).compact
|
|
26
|
+
end
|
|
23
27
|
end
|
|
24
28
|
alias to_a provision_commands
|
|
25
29
|
|
|
@@ -83,7 +87,11 @@ module CemAcpt
|
|
|
83
87
|
|
|
84
88
|
def provision_commands
|
|
85
89
|
commands_from_config = @config.get("images.#{@image_name}.provision_commands") || []
|
|
86
|
-
(
|
|
90
|
+
if @config.get("images.#{@image_name}.skip_default_provision_commands")
|
|
91
|
+
commands_from_config.compact
|
|
92
|
+
else
|
|
93
|
+
(default_provision_commands + commands_from_config).compact
|
|
94
|
+
end
|
|
87
95
|
end
|
|
88
96
|
alias to_a provision_commands
|
|
89
97
|
end
|
|
@@ -103,7 +111,11 @@ module CemAcpt
|
|
|
103
111
|
|
|
104
112
|
def provision_commands
|
|
105
113
|
commands_from_config = @config.get("images.#{@image_name}.provision_commands") || []
|
|
106
|
-
(
|
|
114
|
+
if @config.get("images.#{@image_name}.skip_default_provision_commands")
|
|
115
|
+
commands_from_config.compact
|
|
116
|
+
else
|
|
117
|
+
(default_provision_commands + commands_from_config).compact
|
|
118
|
+
end
|
|
107
119
|
end
|
|
108
120
|
alias to_a provision_commands
|
|
109
121
|
|
|
@@ -7,6 +7,7 @@ require_relative 'platform'
|
|
|
7
7
|
require_relative 'utils'
|
|
8
8
|
require_relative 'version'
|
|
9
9
|
require_relative 'provision/terraform/terraform_cmd'
|
|
10
|
+
require_relative 'image_builder/errors'
|
|
10
11
|
require_relative 'image_builder/exec'
|
|
11
12
|
require_relative 'image_builder/provision_commands'
|
|
12
13
|
|
|
@@ -35,6 +36,10 @@ module CemAcpt
|
|
|
35
36
|
class TerraformBuilder
|
|
36
37
|
DEFAULT_PLAN_NAME = 'testplan.tfplan'
|
|
37
38
|
DEFAULT_VARS_FILE = 'imagevars.json'
|
|
39
|
+
# GCE image names must match RFC 1035 label rules: 1-63 chars, lowercase
|
|
40
|
+
# alphanumeric and dashes, no trailing dash. See
|
|
41
|
+
# https://cloud.google.com/compute/docs/reference/rest/v1/images.
|
|
42
|
+
GCE_IMAGE_NAME_MAX = 63
|
|
38
43
|
include CemAcpt::Logging
|
|
39
44
|
|
|
40
45
|
attr_reader :exit_code, :duration
|
|
@@ -64,6 +69,7 @@ module CemAcpt
|
|
|
64
69
|
if image_types.empty?
|
|
65
70
|
raise 'No images to build. Ensure the images config is populated and that --filter (if used) matches at least one image.'
|
|
66
71
|
end
|
|
72
|
+
image_types.each { |_, os_str| assert_template_present!(os_str) }
|
|
67
73
|
return dry_run(image_types) if @config.get('dry_run')
|
|
68
74
|
|
|
69
75
|
@working_dir = new_working_dir
|
|
@@ -80,7 +86,7 @@ module CemAcpt
|
|
|
80
86
|
output.each do |instance_name, data|
|
|
81
87
|
unless @config.get('no_destroy_nodes')
|
|
82
88
|
logger.info('CemAcpt::ImageBuilder') { "Stopping instance #{instance_name}..." }
|
|
83
|
-
@exec.run('compute', 'instances', 'stop', instance_name)
|
|
89
|
+
@exec.run('compute', 'instances', 'stop', instance_name, '--zone', @config.get('platform.zone'))
|
|
84
90
|
end
|
|
85
91
|
unless @config.get('no_build_images')
|
|
86
92
|
deprecate_old_images_in_family(data['image_family'])
|
|
@@ -109,8 +115,18 @@ module CemAcpt
|
|
|
109
115
|
|
|
110
116
|
attr_reader :environment
|
|
111
117
|
|
|
118
|
+
# Builds a GCE-safe image name from an image family. The returned name
|
|
119
|
+
# is at most GCE_IMAGE_NAME_MAX characters long, preserves the
|
|
120
|
+
# `-v<unix_timestamp>` suffix verbatim (so consecutive builds in the
|
|
121
|
+
# same family cannot collide on the truncated name), and never ends in
|
|
122
|
+
# a dash.
|
|
112
123
|
def image_name_from_image_family(image_family)
|
|
113
|
-
"
|
|
124
|
+
suffix = "-v#{@start_time.to_i}"
|
|
125
|
+
full = "#{image_family}#{suffix}"
|
|
126
|
+
return full if full.length <= GCE_IMAGE_NAME_MAX
|
|
127
|
+
|
|
128
|
+
prefix = image_family[0, GCE_IMAGE_NAME_MAX - suffix.length]
|
|
129
|
+
prefix.sub(/-+\z/, '') + suffix
|
|
114
130
|
end
|
|
115
131
|
|
|
116
132
|
def deprecate_old_images_in_family(image_family)
|
|
@@ -131,6 +147,17 @@ module CemAcpt
|
|
|
131
147
|
logger.info('CemAcpt::ImageBuilder') { "Image #{image_name} created for family #{image_family}"}
|
|
132
148
|
end
|
|
133
149
|
|
|
150
|
+
# Raises MissingTemplateError if the OS bucket has images configured but
|
|
151
|
+
# no `main.tf` ships under `lib/terraform/image/<platform>/<os_str>/`.
|
|
152
|
+
# Without this guard, `terraform init` later fails with an unhelpful
|
|
153
|
+
# "no configuration files" message.
|
|
154
|
+
def assert_template_present!(os_str)
|
|
155
|
+
template_path = File.join(@image_terraform_dir, os_str, 'main.tf')
|
|
156
|
+
return if File.exist?(template_path)
|
|
157
|
+
|
|
158
|
+
raise MissingTemplateError.new(os_str, template_path)
|
|
159
|
+
end
|
|
160
|
+
|
|
134
161
|
def no_windows?
|
|
135
162
|
@windows_tfvars[:node_data].empty? || @config.get('cem_acpt_image.no_windows')
|
|
136
163
|
end
|
|
@@ -85,8 +85,15 @@ module CemAcpt
|
|
|
85
85
|
def character_substitutions(name)
|
|
86
86
|
return name unless @config[:character_substitutions]
|
|
87
87
|
|
|
88
|
+
subs = @config[:character_substitutions]
|
|
89
|
+
unless subs.is_a?(Array) && subs.all? { |s| s.is_a?(Array) && s.size == 2 }
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
'image_name_builder.character_substitutions must be an array of 2-item arrays, ' \
|
|
92
|
+
"e.g. [['_', '-']]; got: #{subs.inspect}"
|
|
93
|
+
end
|
|
94
|
+
|
|
88
95
|
subbed_name = name
|
|
89
|
-
|
|
96
|
+
subs.each do |char_sub|
|
|
90
97
|
subbed_name.gsub!(char_sub[0], char_sub[1])
|
|
91
98
|
end
|
|
92
99
|
subbed_name
|
|
@@ -4,112 +4,118 @@ require 'json'
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
|
|
6
6
|
# GCP platform implementation
|
|
7
|
-
module
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
7
|
+
module CemAcpt
|
|
8
|
+
module Platform
|
|
9
|
+
module Mixin
|
|
10
|
+
module Gcp
|
|
11
|
+
def platform_data
|
|
12
|
+
{
|
|
13
|
+
username: gcp_username,
|
|
14
|
+
credentials_file: gcp_credentials_file,
|
|
15
|
+
project: gcp_project,
|
|
16
|
+
region: gcp_region,
|
|
17
|
+
zone: gcp_zone,
|
|
18
|
+
subnetwork: gcp_subnetwork,
|
|
19
|
+
private_key: gcp_private_key,
|
|
20
|
+
public_key: gcp_public_key,
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def node_data
|
|
25
|
+
nd = {}
|
|
26
|
+
nd[:machine_type] = gcp_machine_type
|
|
27
|
+
nd[:disk_size] = gcp_disk_size
|
|
28
|
+
nd[:max_run_duration] = gcp_max_run_duration
|
|
29
|
+
nd[:image] = image_name if image_name
|
|
30
|
+
nd[:test_name] = @test_data[:test_name] if @test_data&.key?(:test_name)
|
|
31
|
+
nd
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def gcp_username
|
|
35
|
+
return @gcp_username if @gcp_username
|
|
36
|
+
|
|
37
|
+
@gcp_username = @config.get('platform.username')
|
|
38
|
+
return @gcp_username if @gcp_username
|
|
39
|
+
|
|
40
|
+
stdout, stderr, status = Open3.capture3('gcloud compute os-login describe-profile --format json')
|
|
41
|
+
raise "gcloud os-login describe-profile failed (exit #{status.exitstatus}): #{stderr.chomp}" unless status.success?
|
|
42
|
+
|
|
43
|
+
parsed = JSON.parse(stdout)
|
|
44
|
+
accounts = parsed['posixAccounts']
|
|
45
|
+
raise "gcloud os-login profile returned no posixAccounts. stderr: #{stderr.chomp}" if accounts.nil? || accounts.empty?
|
|
46
|
+
|
|
47
|
+
username = accounts.first['username']
|
|
48
|
+
raise "gcloud os-login posixAccounts entry has no username. stdout: #{stdout.chomp}" unless username
|
|
49
|
+
|
|
50
|
+
@gcp_username = username
|
|
51
|
+
rescue JSON::ParserError => e
|
|
52
|
+
raise "Failed to parse gcloud os-login output: #{e.message}. stdout: #{stdout.chomp}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def gcp_credentials_file
|
|
56
|
+
@gcp_credentials_file ||= (@config.get('platform.credentials_file') || File.join(Dir.home, '.config', 'gcloud', 'application_default_credentials.json'))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def gcp_project
|
|
60
|
+
return @gcp_project if @gcp_project
|
|
61
|
+
|
|
62
|
+
configured = @config.get('platform.project')
|
|
63
|
+
return @gcp_project = configured if configured
|
|
64
|
+
|
|
65
|
+
stdout, stderr, status = Open3.capture3('gcloud config get-value project')
|
|
66
|
+
raise "gcloud config get-value project failed (exit #{status.exitstatus}): #{stderr.chomp}" unless status.success?
|
|
67
|
+
|
|
68
|
+
@gcp_project = stdout.chomp
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def gcp_region
|
|
72
|
+
return @gcp_region if @gcp_region
|
|
73
|
+
|
|
74
|
+
configured = @config.get('platform.region')
|
|
75
|
+
return @gcp_region = configured if configured
|
|
76
|
+
|
|
77
|
+
stdout, stderr, status = Open3.capture3('gcloud config get-value compute/region')
|
|
78
|
+
raise "gcloud config get-value compute/region failed (exit #{status.exitstatus}): #{stderr.chomp}" unless status.success?
|
|
79
|
+
|
|
80
|
+
@gcp_region = stdout.chomp
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def gcp_zone
|
|
84
|
+
return @gcp_zone if @gcp_zone
|
|
85
|
+
|
|
86
|
+
configured = @config.get('platform.zone')
|
|
87
|
+
return @gcp_zone = configured if configured
|
|
88
|
+
|
|
89
|
+
stdout, stderr, status = Open3.capture3('gcloud config get-value compute/zone')
|
|
90
|
+
raise "gcloud config get-value compute/zone failed (exit #{status.exitstatus}): #{stderr.chomp}" unless status.success?
|
|
91
|
+
|
|
92
|
+
@gcp_zone = stdout.chomp
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def gcp_subnetwork
|
|
96
|
+
@gcp_subnetwork ||= (@config.get('platform.subnetwork') || 'default')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def gcp_private_key
|
|
100
|
+
@gcp_private_key ||= (@run_data[:private_key] || File.join(Dir.home, '.ssh', 'google_compute_engine'))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def gcp_public_key
|
|
104
|
+
@gcp_public_key ||= (@run_data[:public_key] || File.join(Dir.home, '.ssh', 'google_compute_engine.pub'))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def gcp_machine_type
|
|
108
|
+
@gcp_machine_type ||= (@config.get('node_data.machine_type') || 'e2-medium')
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def gcp_disk_size
|
|
112
|
+
@gcp_disk_size ||= (@config.get('node_data.disk_size') || 40)
|
|
113
|
+
end
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
def gcp_max_run_duration
|
|
116
|
+
@gcp_max_run_duration ||= (@config.get('node_data.max_run_duration') || 3600)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
114
120
|
end
|
|
115
121
|
end
|
data/lib/cem_acpt/platform.rb
CHANGED
|
@@ -7,6 +7,13 @@ require_relative 'logging'
|
|
|
7
7
|
module CemAcpt::Platform
|
|
8
8
|
class Error < StandardError; end
|
|
9
9
|
|
|
10
|
+
# Namespace for per-platform mixin modules. Each platform file in
|
|
11
|
+
# +lib/cem_acpt/platform/<name>.rb+ defines a module
|
|
12
|
+
# +CemAcpt::Platform::Mixin::<CamelCaseName>+ providing
|
|
13
|
+
# +#platform_data+ and +#node_data+, which is included into the
|
|
14
|
+
# dynamically-generated platform class.
|
|
15
|
+
module Mixin; end
|
|
16
|
+
|
|
10
17
|
PLATFORM_DIR = File.expand_path(File.join(__dir__, 'platform'))
|
|
11
18
|
BASE_TYPES = %i[base test].freeze
|
|
12
19
|
|
|
@@ -77,23 +84,19 @@ module CemAcpt::Platform
|
|
|
77
84
|
# @param platform [String] the name of the platform.
|
|
78
85
|
# @return [Class] the platform-specific Object.
|
|
79
86
|
def platform_class(base_type, platform)
|
|
80
|
-
|
|
87
|
+
const_name = platform.split(/[_-]/).map(&:capitalize).join
|
|
81
88
|
# We require the platform base class here so that we can use it as
|
|
82
89
|
# a parent class for the platform-specific class.
|
|
83
90
|
require_relative 'platform/base'
|
|
84
|
-
# If the class has already been defined
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
# If the class has already been defined under CemAcpt::Platform, reuse
|
|
92
|
+
# it. The +false+ argument skips ancestor lookup so that an unrelated
|
|
93
|
+
# constant of the same name elsewhere in the constant graph cannot
|
|
94
|
+
# silently win the cache check.
|
|
95
|
+
if CemAcpt::Platform.const_defined?(const_name, false)
|
|
96
|
+
logger.debug('CemAcpt::Platform') { "Using existing platform class #{const_name}" }
|
|
97
|
+
klass = CemAcpt::Platform.const_get(const_name, false)
|
|
88
98
|
else
|
|
89
|
-
|
|
90
|
-
# a new constant with the name of the platform capitalized, and
|
|
91
|
-
# associate that constant with a new instance of Class that inherits
|
|
92
|
-
# from the platform base class. We then require the platform file,
|
|
93
|
-
# include and extend our class with the Platform module from the file,
|
|
94
|
-
# include Logging and Concurrent::Async, and finally call the
|
|
95
|
-
# initialize method on the class.
|
|
96
|
-
logger.debug "Creating platform class #{class_name}"
|
|
99
|
+
logger.debug "Creating platform class #{const_name}"
|
|
97
100
|
baseklass = case base_type.to_sym
|
|
98
101
|
when :base
|
|
99
102
|
CemAcpt::Platform::Base
|
|
@@ -102,12 +105,11 @@ module CemAcpt::Platform
|
|
|
102
105
|
else
|
|
103
106
|
raise Error, "Base type #{base_type} is not supported"
|
|
104
107
|
end
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end,
|
|
108
|
+
require_relative "platform/#{platform}"
|
|
109
|
+
mixin = CemAcpt::Platform::Mixin.const_get(const_name, false)
|
|
110
|
+
klass = CemAcpt::Platform.const_set(
|
|
111
|
+
const_name,
|
|
112
|
+
Class.new(baseklass) { include mixin },
|
|
111
113
|
)
|
|
112
114
|
end
|
|
113
115
|
logger.debug('CemAcpt::Platform') { "Using platform class: #{klass.inspect}" }
|
|
@@ -9,6 +9,29 @@ module CemAcpt
|
|
|
9
9
|
include CemAcpt::Logging
|
|
10
10
|
extend CemAcpt::Logging
|
|
11
11
|
|
|
12
|
+
# Raised when a test name cannot be classified as either a Linux or Windows test by `os_family_for`.
|
|
13
|
+
class UnknownOsFamilyError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Classifies a test name as `:linux` or `:windows` using the same `use_for?` predicate that
|
|
16
|
+
# `Provision::Terraform#new_backend` consumes. Anchored on `Linux.valid_names` /
|
|
17
|
+
# `Windows.valid_names` rather than substring matching, so a test like
|
|
18
|
+
# `cis_rhel-8_firewalld_windowserver_2` correctly classifies as `:linux`.
|
|
19
|
+
# @param test_name [String] The test name to classify.
|
|
20
|
+
# @return [Symbol] `:windows` or `:linux`.
|
|
21
|
+
# @raise [UnknownOsFamilyError] if the test name does not match any known OS / version.
|
|
22
|
+
def self.os_family_for(test_name)
|
|
23
|
+
return :windows if CemAcpt::Provision::Windows.use_for?(test_name)
|
|
24
|
+
return :linux if CemAcpt::Provision::Linux.use_for?(test_name)
|
|
25
|
+
|
|
26
|
+
raise UnknownOsFamilyError, [
|
|
27
|
+
"Cannot determine OS family for test name: #{test_name}.",
|
|
28
|
+
"Known OSes: linux=[#{CemAcpt::Provision::Linux.valid_names.join(', ')}], " \
|
|
29
|
+
"windows=[#{CemAcpt::Provision::Windows.valid_names.join(', ')}].",
|
|
30
|
+
"Known versions: linux=[#{CemAcpt::Provision::Linux.valid_versions.join(', ')}], " \
|
|
31
|
+
"windows=[#{CemAcpt::Provision::Windows.valid_versions.join(', ')}].",
|
|
32
|
+
].join(' ')
|
|
33
|
+
end
|
|
34
|
+
|
|
12
35
|
# Determines if this OsData implementation should be used for the given test name. This method extracts the OS
|
|
13
36
|
# name and version from the test name using a regular expression, and checks if they match the valid names and
|
|
14
37
|
# versions defined by the subclass. The test name is expected to be in the format `<prefix>_osname-version`, where
|
|
@@ -23,8 +23,14 @@ module CemAcpt
|
|
|
23
23
|
'C:/cem_acpt'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Windows provisioning intentionally does not run any Terraform
|
|
27
|
+
# `remote-exec` commands — the Windows `main.tf` has no provisioner
|
|
28
|
+
# blocks. All shell work happens via {CemAcpt::Utils::WinRMRunner}
|
|
29
|
+
# after Terraform finishes. See ARCHITECTURE.md §13.
|
|
26
30
|
def provision_commands
|
|
27
|
-
|
|
31
|
+
raise NotImplementedError,
|
|
32
|
+
'Windows provisioning is performed via Utils::WinRMRunner, not via ' \
|
|
33
|
+
'Terraform remote-exec. See ARCHITECTURE.md §13.'
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
end
|
|
@@ -172,32 +172,23 @@ module CemAcpt
|
|
|
172
172
|
terraform.show({ chdir: working_dir, no_color: true }, { environment: environment })
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
-
# Creates a new backend instance based on the OS specified in the test name.
|
|
176
|
-
#
|
|
177
|
-
# of the appropriate class. If the test name does not match any known OS, an error is raised.
|
|
175
|
+
# Creates a new backend instance based on the OS specified in the test name. Delegates to
|
|
176
|
+
# `Provision::OsData.os_family_for` so the runner and provisioner share a single source of truth for OS dispatch.
|
|
178
177
|
# @param test_name [String] The name of the test, which should include the OS name and version in the format
|
|
179
178
|
# `<prefix>_osname-version`.
|
|
180
179
|
# @return [CemAcpt::Provision::Linux, CemAcpt::Provision::Windows] An instance of the appropriate backend class
|
|
181
180
|
# based on the OS specified in the test name.
|
|
182
|
-
# @raise [
|
|
181
|
+
# @raise [CemAcpt::Provision::OsData::UnknownOsFamilyError] If the test name does not match any known OS or version.
|
|
183
182
|
def new_backend(test_name)
|
|
184
|
-
|
|
183
|
+
case CemAcpt::Provision::OsData.os_family_for(test_name)
|
|
184
|
+
when :linux
|
|
185
185
|
logger.info('CemAcpt::Provision::Terraform') { 'Using Linux backend' }
|
|
186
186
|
logger.verbose('CemAcpt::Provision::Terraform') { "Creating backend with provision_data:\n#{JSON.pretty_generate(@provision_data)}" }
|
|
187
187
|
CemAcpt::Provision::Linux.new(@config, @provision_data)
|
|
188
|
-
|
|
188
|
+
when :windows
|
|
189
189
|
logger.info('CemAcpt::Provision::Terraform') { 'Using Windows backend' }
|
|
190
190
|
logger.verbose('CemAcpt::Provision::Terraform') { "Creating backend with provision_data:\n#{JSON.pretty_generate(@provision_data)}" }
|
|
191
191
|
CemAcpt::Provision::Windows.new(@config, @provision_data)
|
|
192
|
-
else
|
|
193
|
-
err_msg = [
|
|
194
|
-
"Test name #{test_name} does not match any known OS.",
|
|
195
|
-
"Known OSes are: #{CemAcpt::Provision::Linux.valid_names.join(', ')}",
|
|
196
|
-
"and #{CemAcpt::Provision::Windows.valid_names.join(', ')}.",
|
|
197
|
-
"Known versions are: #{CemAcpt::Provision::Linux.valid_versions.join(', ')}",
|
|
198
|
-
", and #{CemAcpt::Provision::Windows.valid_versions.join(', ')}.",
|
|
199
|
-
].join(' ')
|
|
200
|
-
raise ArgumentError, err_msg
|
|
201
192
|
end
|
|
202
193
|
end
|
|
203
194
|
|
|
@@ -282,7 +273,7 @@ module CemAcpt
|
|
|
282
273
|
puppet_manifest: node.test_data[:puppet_manifest],
|
|
283
274
|
provision_dir_source: @backend.provision_directory,
|
|
284
275
|
provision_dir_dest: @backend.destination_provision_directory,
|
|
285
|
-
provision_commands:
|
|
276
|
+
provision_commands: provision_commands_for(node),
|
|
286
277
|
}
|
|
287
278
|
)
|
|
288
279
|
end
|
|
@@ -291,6 +282,19 @@ module CemAcpt
|
|
|
291
282
|
raise e
|
|
292
283
|
end
|
|
293
284
|
|
|
285
|
+
# Windows provisioning runs via {CemAcpt::Utils::WinRMRunner} rather
|
|
286
|
+
# than Terraform `remote-exec`, so its node_data carries no commands.
|
|
287
|
+
def provision_commands_for(node)
|
|
288
|
+
case @backend
|
|
289
|
+
when CemAcpt::Provision::Linux
|
|
290
|
+
@backend.provision_commands_wrapper(node.node_data[:image])
|
|
291
|
+
when CemAcpt::Provision::Windows
|
|
292
|
+
[]
|
|
293
|
+
else
|
|
294
|
+
@backend.provision_commands
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
294
298
|
# @return [Hash] The variables to be passed to Terraform, including the provision data for the nodes and any
|
|
295
299
|
# necessary credentials and module package paths.
|
|
296
300
|
# @raise [StandardError] If there is an error formatting the variables.
|
|
@@ -6,7 +6,8 @@ require_relative 'base'
|
|
|
6
6
|
module CemAcpt
|
|
7
7
|
module TestRunner
|
|
8
8
|
module LogFormatter
|
|
9
|
-
#
|
|
9
|
+
# Canonical formatter for Bolt subsystem results. Wired into
|
|
10
|
+
# LogFormatter.new_formatter for CemAcpt::Bolt::SummaryResults.
|
|
10
11
|
class BoltSummaryResultsFormatter < Base
|
|
11
12
|
def initialize(config, instance_names_ips, subject: nil)
|
|
12
13
|
super(subject)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'log_formatter/bolt_summary_results_formatter'
|
|
4
|
-
require_relative 'log_formatter/bolt_output_formatter'
|
|
5
4
|
require_relative 'log_formatter/goss_action_response'
|
|
6
5
|
require_relative 'log_formatter/goss_error_formatter'
|
|
7
6
|
require_relative 'log_formatter/standard_error_formatter'
|