cem_acpt 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +68 -0
- data/README.md +146 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cem_acpt.gemspec +37 -0
- data/exe/cem_acpt +58 -0
- data/lib/cem_acpt/bootstrap/bootstrapper.rb +206 -0
- data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +129 -0
- data/lib/cem_acpt/bootstrap/operating_system.rb +17 -0
- data/lib/cem_acpt/bootstrap.rb +12 -0
- data/lib/cem_acpt/context.rb +60 -0
- data/lib/cem_acpt/core_extensions.rb +111 -0
- data/lib/cem_acpt/image_name_builder.rb +104 -0
- data/lib/cem_acpt/logging.rb +193 -0
- data/lib/cem_acpt/platform/base/cmd.rb +65 -0
- data/lib/cem_acpt/platform/base.rb +78 -0
- data/lib/cem_acpt/platform/gcp/cmd.rb +313 -0
- data/lib/cem_acpt/platform/gcp/compute.rb +327 -0
- data/lib/cem_acpt/platform/gcp.rb +85 -0
- data/lib/cem_acpt/platform/vmpooler.rb +24 -0
- data/lib/cem_acpt/platform.rb +103 -0
- data/lib/cem_acpt/puppet_helpers.rb +38 -0
- data/lib/cem_acpt/runner.rb +304 -0
- data/lib/cem_acpt/shared_objects.rb +416 -0
- data/lib/cem_acpt/spec_helper_acceptance.rb +176 -0
- data/lib/cem_acpt/test_data.rb +157 -0
- data/lib/cem_acpt/utils.rb +70 -0
- data/lib/cem_acpt/version.rb +5 -0
- data/lib/cem_acpt.rb +27 -0
- data/sample_config.yaml +58 -0
- metadata +195 -0
@@ -0,0 +1,327 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CemAcpt::Platform::Gcp
|
4
|
+
require_relative 'cmd'
|
5
|
+
require_relative File.join(__dir__, '..', '..', 'logging.rb')
|
6
|
+
|
7
|
+
module Helper
|
8
|
+
def add_cmd(cmd_obj)
|
9
|
+
@cmd = cmd_obj
|
10
|
+
end
|
11
|
+
|
12
|
+
def cmd
|
13
|
+
@cmd ||= CemAcpt::Platform::Gcp::Cmd.new(out_format: 'json')
|
14
|
+
end
|
15
|
+
|
16
|
+
def cmd_flag_vars?(*vars)
|
17
|
+
vars.none? { |v| v.nil? || (v.empty? if v.respond_to?(:empty?)) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# This class represents a GCP project
|
22
|
+
class Project
|
23
|
+
include Helper
|
24
|
+
|
25
|
+
def initialize(name: nil, zone: nil)
|
26
|
+
@name = name
|
27
|
+
@zone = zone
|
28
|
+
end
|
29
|
+
|
30
|
+
def name
|
31
|
+
@name ||= cmd.project_from_config
|
32
|
+
end
|
33
|
+
|
34
|
+
def zone
|
35
|
+
@zone ||= cmd.zone_from_config
|
36
|
+
end
|
37
|
+
|
38
|
+
def cmd_flag
|
39
|
+
"--project=#{name}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# This class represents a GCP VM service account
|
44
|
+
class ServiceAccount
|
45
|
+
include Helper
|
46
|
+
|
47
|
+
attr_reader :name, :scopes
|
48
|
+
|
49
|
+
def initialize(name: nil, scopes: ['devstorage.read_only', 'logging.write', 'monitoring.write', 'servicecontrol', 'service.management.readonly', 'trace.append' ])
|
50
|
+
@name = name
|
51
|
+
@scopes = scopes.map { |s| "https://www.googleapis.com/auth/#{s}" }
|
52
|
+
end
|
53
|
+
|
54
|
+
def cmd_flag
|
55
|
+
"--service-account=#{name} --scopes=#{scopes.join(',')}" if cmd_flag_vars?(name, scopes)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# This class represents a GCP VM disk
|
60
|
+
class Disk
|
61
|
+
include Helper
|
62
|
+
|
63
|
+
attr_reader :project, :name, :size, :image_name
|
64
|
+
|
65
|
+
def initialize(project: nil, name: 'disk-1', image_name: '', size: 20, type: 'pd-standard')
|
66
|
+
@project = project
|
67
|
+
@name = name
|
68
|
+
@image_name = image_name
|
69
|
+
@size = size
|
70
|
+
@type = type
|
71
|
+
end
|
72
|
+
|
73
|
+
def cmd_flag
|
74
|
+
return unless cmd_flag_vars?(name, image, size, type)
|
75
|
+
|
76
|
+
'--create-disk=auto-delete=yes,boot=yes,' \
|
77
|
+
"device-name=#{name},image=#{image}," \
|
78
|
+
"mode=rw,size=#{size},type=#{type}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def type
|
82
|
+
type_path(@type)
|
83
|
+
end
|
84
|
+
|
85
|
+
def image
|
86
|
+
@image ||= image_path(@image_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def image_path(image_str)
|
92
|
+
raise ArgumentError, 'image must be a non-empty string' unless image_str.is_a?(String) && !image_str.empty?
|
93
|
+
|
94
|
+
data = cmd.local_exec('compute images list', out_filter: "name ~ '#{image_str}$'").first
|
95
|
+
if data.nil? || data.empty?
|
96
|
+
data = cmd.local_exec('compute images list', out_filter: "family ~ '#{image_str}$'").first
|
97
|
+
end
|
98
|
+
raise "Image #{image_str} not found" unless data
|
99
|
+
raise "Image status invalid for image #{data['selfLink']}: #{data[status]}" unless data['status'] == 'READY'
|
100
|
+
|
101
|
+
data['selfLink'].split('v1/')[-1]
|
102
|
+
end
|
103
|
+
|
104
|
+
def type_path(type)
|
105
|
+
raise ArgumentError, 'type must be a non-empty string' unless type.is_a?(String) && !type.empty?
|
106
|
+
|
107
|
+
"projects/#{project.name}/zones/#{project.zone}/diskTypes/#{type}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# This class represents a GCP VM network interface
|
112
|
+
class NetworkInterface
|
113
|
+
include Helper
|
114
|
+
|
115
|
+
attr_reader :tier, :subnetwork
|
116
|
+
|
117
|
+
def initialize(tier: 'STANDARD', subnetwork: 'default')
|
118
|
+
@tier = tier.upcase
|
119
|
+
@subnetwork = subnetwork
|
120
|
+
end
|
121
|
+
|
122
|
+
def cmd_flag
|
123
|
+
"--network-interface=network-tier=#{tier},subnet=#{subnetwork}" if cmd_flag_vars?(tier, subnetwork)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# This class represents GCP VM metadata
|
128
|
+
class Metadata
|
129
|
+
include Helper
|
130
|
+
|
131
|
+
def initialize(**data)
|
132
|
+
@data = data.transform_keys(&:to_s)
|
133
|
+
end
|
134
|
+
|
135
|
+
def data
|
136
|
+
format_data(@data)
|
137
|
+
end
|
138
|
+
|
139
|
+
def cmd_flag
|
140
|
+
return unless cmd_flag_vars?(data)
|
141
|
+
|
142
|
+
data_from_file = data.select { |k, _v| k == 'ssh-keys' }.map { |k, v| "#{k}=#{v}" }.join(',')
|
143
|
+
normalized_data = data.reject { |k, _v| k == 'ssh-keys' }.map { |k, v| "#{k}=#{v}" }.join(',')
|
144
|
+
cmd = []
|
145
|
+
cmd << "--metadata-from-file=#{data_from_file}" unless data_from_file.empty?
|
146
|
+
cmd << "--metadata=#{normalized_data}" unless normalized_data.empty?
|
147
|
+
cmd.join(' ')
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def format_data(data)
|
153
|
+
default_data = { 'enable-oslogin' => 'TRUE' }
|
154
|
+
if data['ephemeral_ssh_key']
|
155
|
+
args = {
|
156
|
+
username: (data.dig('ephemeral_ssh_key', 'username') || cmd.user_name),
|
157
|
+
}
|
158
|
+
args[:lifetime] = data.dig('ephemeral_ssh_key', 'lifetime') if data.dig('ephemeral_ssh_key', 'lifetime')
|
159
|
+
args[:keydir] = data.dig('ephemeral_ssh_key', 'keydir') if data.dig('ephemeral_ssh_key', 'keydir')
|
160
|
+
priv_key_path, ssh_keys = ephemeral_ssh_key(**args)
|
161
|
+
default_data['priv_key_local_path'] = priv_key_path
|
162
|
+
default_data['ssh-keys'] = ssh_keys
|
163
|
+
data.delete('ephemeral_ssh_key')
|
164
|
+
end
|
165
|
+
default_data.merge(data)
|
166
|
+
end
|
167
|
+
|
168
|
+
def ephemeral_ssh_key(username: nil, lifetime: 2, keydir: '/tmp/acpt_ssh_keys')
|
169
|
+
raise ArgumentError, 'username must be a non-empty string' unless username.is_a?(String) && !username.empty?
|
170
|
+
|
171
|
+
keypath = File.join(keydir, SecureRandom.hex(10))
|
172
|
+
Dir.mkdir(keydir) unless Dir.exist?(keydir)
|
173
|
+
lifetime_in_seconds = (lifetime.to_i * 60 * 60)
|
174
|
+
iso8601_expiration = (Time.now.utc + lifetime_in_seconds).strftime('%Y-%m-%dT%H:%M:%S%z')
|
175
|
+
keygen_cmd = [
|
176
|
+
'ssh-keygen',
|
177
|
+
'-t ecdsa',
|
178
|
+
'-b 521',
|
179
|
+
"-C 'google-ssh {\"userName\":\"#{username}\",\"expireOn\":\"#{iso8601_expiration}\"}'",
|
180
|
+
"-f #{keypath}",
|
181
|
+
'-N ""',
|
182
|
+
'-q',
|
183
|
+
].join(' ')
|
184
|
+
_, stderr, status = Open3.capture3(keygen_cmd)
|
185
|
+
raise stderr unless status.success?
|
186
|
+
|
187
|
+
File.write("#{keypath}.pub", "#{username}:#{File.read("#{keypath}.pub")}")
|
188
|
+
[keypath, "#{keypath}.pub"]
|
189
|
+
rescue StandardError => e
|
190
|
+
raise "Failed to generate ephemeral SSH key: #{e} #{e.backtrace}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# This class represents a GCP VM. It is composed of various component classes.
|
195
|
+
class VM
|
196
|
+
include Helper
|
197
|
+
include CemAcpt::Logging
|
198
|
+
|
199
|
+
attr_accessor :name, :project, :service_account, :disk, :network_interface, :machine_type, :metadata
|
200
|
+
attr_reader :cmd
|
201
|
+
|
202
|
+
def initialize(name, components: {})
|
203
|
+
@name = name
|
204
|
+
@components = components
|
205
|
+
@machine_type = components[:machine_type]
|
206
|
+
@cmd = CemAcpt::Platform::Gcp::Cmd.new(
|
207
|
+
project: components[:project][:name],
|
208
|
+
zone: components[:project][:zone],
|
209
|
+
out_format: 'json',
|
210
|
+
local_port: components[:local_port],
|
211
|
+
)
|
212
|
+
@configured = false
|
213
|
+
end
|
214
|
+
|
215
|
+
def configure!
|
216
|
+
return @configured if @configured
|
217
|
+
|
218
|
+
@project = Project.new(**@components[:project])
|
219
|
+
@project.add_cmd(@cmd)
|
220
|
+
@service_account = @components.key?(:service_account) ? ServiceAccount.new(**@components[:service_account]) : nil
|
221
|
+
@disk = Disk.new(project: @project, **@components[:disk])
|
222
|
+
@disk.add_cmd(@cmd)
|
223
|
+
@network_interface = NetworkInterface.new(**@components[:network_interface])
|
224
|
+
@machine_type = @components[:machine_type]
|
225
|
+
@metadata = Metadata.new(**@components[:metadata])
|
226
|
+
@metadata.add_cmd(@cmd)
|
227
|
+
@configured = true
|
228
|
+
end
|
229
|
+
|
230
|
+
def info
|
231
|
+
describe_cmd = "compute instances describe #{name}"
|
232
|
+
logger.debug("Gathering info for VM #{name} with gcloud command: #{describe_cmd}")
|
233
|
+
data = @cmd.local_exec(describe_cmd, out_format: 'json')
|
234
|
+
opts = @cmd.ssh_opts(instance_name: name)
|
235
|
+
{
|
236
|
+
node_data: data,
|
237
|
+
transport: :ssh,
|
238
|
+
ssh_opts: opts,
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
def create
|
243
|
+
@cmd.local_exec(create_cmd)
|
244
|
+
rescue StandardError => e
|
245
|
+
raise "Failed to create VM #{name} with command #{create_cmd}: #{e}"
|
246
|
+
end
|
247
|
+
|
248
|
+
def ready?
|
249
|
+
logger.debug("Checking if VM #{name} is ready")
|
250
|
+
instance_status = @cmd.local_exec('compute instances list', out_filter: "NAME = #{name}").first['status']
|
251
|
+
logger.debug("Instance #{name} status: #{instance_status}")
|
252
|
+
return false unless instance_status == 'RUNNING'
|
253
|
+
|
254
|
+
logger.debug("Checking instance #{name} SSH connectivity")
|
255
|
+
@cmd.ssh_ready?(name)
|
256
|
+
rescue StandardError
|
257
|
+
false
|
258
|
+
end
|
259
|
+
|
260
|
+
def destroy
|
261
|
+
@cmd.delete_instance(name)
|
262
|
+
end
|
263
|
+
|
264
|
+
def install_puppet_module_package(module_pkg_path, remote_path, puppet_path)
|
265
|
+
@cmd.scp_upload(@name, module_pkg_path, remote_path)
|
266
|
+
@cmd.ssh(@name, "sudo #{puppet_path} module install #{remote_path}")
|
267
|
+
end
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
# Determines whether the given instance name is a fully qualified
|
272
|
+
# hostname or not.
|
273
|
+
def hostname?(instance_name)
|
274
|
+
instance_name.include?('.')
|
275
|
+
end
|
276
|
+
|
277
|
+
# Validates a GCP instance name.
|
278
|
+
def validate_instance_name(instance_name)
|
279
|
+
if instance_name.nil? || !instance_name.is_a?(String) || instance_name.empty?
|
280
|
+
raise ArgumentError, 'Instance name must be a non-empty string'
|
281
|
+
end
|
282
|
+
raise ArgumentError, 'Instance name must be less than 253 characters' if instance_name.length > 253
|
283
|
+
|
284
|
+
return true unless hostname?(instance_name)
|
285
|
+
|
286
|
+
labels = instance_name.split('.')
|
287
|
+
labels.each do |label|
|
288
|
+
unless label.match?(%r{^[a-z]([-a-z0-9]*[a-z0-9])?$})
|
289
|
+
raise ArgumentError, "Instance name portion #{label} contains invalid characters"
|
290
|
+
end
|
291
|
+
raise ArgumentError, "Instance name portion #{label} is too long" if label.length > 63
|
292
|
+
end
|
293
|
+
true
|
294
|
+
end
|
295
|
+
|
296
|
+
def create_cmd
|
297
|
+
validate_instance_name(name)
|
298
|
+
if hostname?(name)
|
299
|
+
instance_name = name.split('.')[0]
|
300
|
+
hostname = name
|
301
|
+
else
|
302
|
+
instance_name = name
|
303
|
+
hostname = nil
|
304
|
+
end
|
305
|
+
|
306
|
+
cmd = [
|
307
|
+
'compute',
|
308
|
+
'instances',
|
309
|
+
'create',
|
310
|
+
instance_name,
|
311
|
+
'--async',
|
312
|
+
"--machine-type=#{machine_type}",
|
313
|
+
'--maintenance-policy=MIGRATE',
|
314
|
+
'--no-shielded-secure-boot',
|
315
|
+
'--shielded-vtpm',
|
316
|
+
'--shielded-integrity-monitoring',
|
317
|
+
'--reservation-affinity=any',
|
318
|
+
'--tags=acpt-test-node',
|
319
|
+
]
|
320
|
+
[disk&.cmd_flag, network_interface&.cmd_flag, metadata&.cmd_flag, service_account&.cmd_flag].each do |flag|
|
321
|
+
cmd << flag unless flag.nil?
|
322
|
+
end
|
323
|
+
cmd << "--hostname=#{hostname}" unless hostname.nil?
|
324
|
+
cmd.join(' ')
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# GCP platform implementation
|
4
|
+
module Platform
|
5
|
+
require_relative 'gcp/compute'
|
6
|
+
require_relative 'gcp/cmd'
|
7
|
+
|
8
|
+
# Returns information about the GCP instance
|
9
|
+
def node
|
10
|
+
@instance.info
|
11
|
+
end
|
12
|
+
|
13
|
+
# Provision a GCP instance
|
14
|
+
def provision
|
15
|
+
creation_params = config.dup
|
16
|
+
creation_params[:disk][:image_name] = image_name
|
17
|
+
creation_params[:local_port] = local_port
|
18
|
+
@instance = CemAcpt::Platform::Gcp::VM.new(
|
19
|
+
node_name,
|
20
|
+
components: creation_params,
|
21
|
+
)
|
22
|
+
@instance.configure!
|
23
|
+
logger.debug("Creating with command: #{@instance.send(:create_cmd)}")
|
24
|
+
@instance.create
|
25
|
+
end
|
26
|
+
|
27
|
+
# Destroy a GCP instance
|
28
|
+
def destroy
|
29
|
+
@instance.destroy
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns true if the GCP instance is ready for use in the test suite
|
33
|
+
def ready?
|
34
|
+
logger.debug("Checking if #{node_name} is ready...")
|
35
|
+
@instance.ready?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Runs the test suite against the GCP instance. Must be given a block.
|
39
|
+
# If necessary, can pass information into the block to be used in the test suite.
|
40
|
+
def run_tests(&block)
|
41
|
+
logger.debug("Running tests for #{node_name}...")
|
42
|
+
block.call
|
43
|
+
end
|
44
|
+
|
45
|
+
# Uploads and installs a Puppet module package on the GCP instance.
|
46
|
+
def install_puppet_module_package(module_pkg_path, remote_path = nil, puppet_path = '/opt/puppetlabs/bin/puppet')
|
47
|
+
remote_path = remote_path.nil? ? File.join('/tmp', File.basename(module_pkg_path)) : remote_path
|
48
|
+
logger.info("Uploading module package #{module_pkg_path} to #{remote_path} on #{node_name} and installing it...")
|
49
|
+
logger.debug("Using puppet path: #{puppet_path}")
|
50
|
+
@instance.install_puppet_module_package(module_pkg_path, remote_path, puppet_path)
|
51
|
+
logger.info("Module package #{module_pkg_path} installed on #{node_name}")
|
52
|
+
end
|
53
|
+
|
54
|
+
# Extends the class with class methods from the SpecMethods module
|
55
|
+
def self.included(base)
|
56
|
+
base.extend(SpecMethods)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Holds class methods called from spec tests.
|
60
|
+
module SpecMethods
|
61
|
+
# Returns an instance of the GCP platform class command provider
|
62
|
+
# @return [CemAcpt::Platform::Gcp::Cmd]
|
63
|
+
def command_provider
|
64
|
+
CemAcpt::Platform::Gcp::Cmd.new(out_format: 'json')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Apllies the given Puppet manifest on the given instance
|
68
|
+
# @param instance_name [String] the name of the instance to apply the manifest to
|
69
|
+
# @param manifest [String] the Puppet manifest to apply
|
70
|
+
# @param opts [Hash] options to pass to the apply command
|
71
|
+
# @return [String] the output of the apply command
|
72
|
+
def apply_manifest(instance_name, manifest, opts = {})
|
73
|
+
CemAcpt::Platform::Gcp::Cmd.new(out_format: 'json').apply_manifest(instance_name, manifest, opts)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Runs a shell command on the given instance
|
77
|
+
# @param instance_name [String] the name of the instance to run the command on
|
78
|
+
# @param command [String] the command to run
|
79
|
+
# @param opts [Hash] options to pass to the run_shell command
|
80
|
+
# @return [String] the output of the run_shell command
|
81
|
+
def run_shell(instance_name, cmd, opts = {})
|
82
|
+
CemAcpt::Platform::Gcp::Cmd.new(out_format: 'json').run_shell(instance_name, cmd, opts)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Currently a placeholder for future functionality
|
4
|
+
module Platform
|
5
|
+
def node
|
6
|
+
logger.debug 'VMPooler node'
|
7
|
+
end
|
8
|
+
|
9
|
+
def provision
|
10
|
+
logger.debug 'VMPooler provision'
|
11
|
+
end
|
12
|
+
|
13
|
+
def destroy
|
14
|
+
logger.debug 'VMPooler destroy'
|
15
|
+
end
|
16
|
+
|
17
|
+
def ready?
|
18
|
+
logger.debug 'VMPooler ready?'
|
19
|
+
end
|
20
|
+
|
21
|
+
def install_puppet_module_package
|
22
|
+
logger.debug 'VMPooler install_puppet_module_package'
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent-ruby'
|
4
|
+
|
5
|
+
# CemAcpt::Platform manages creating and configring platform specific objects
|
6
|
+
# for the acceptance test suites.
|
7
|
+
module CemAcpt::Platform
|
8
|
+
require_relative 'logging'
|
9
|
+
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
PLATFORM_DIR = File.expand_path(File.join(__dir__, 'platform'))
|
13
|
+
|
14
|
+
class << self
|
15
|
+
include CemAcpt::Logging
|
16
|
+
|
17
|
+
# Creates a new platform specific object of the given platform for each
|
18
|
+
# item in the test data.
|
19
|
+
# @param platform [String] the name of the platform
|
20
|
+
# @param config [CemAcpt::Config] the config object
|
21
|
+
# @param test_data [Hash] the test data
|
22
|
+
# @param local_port_allocator [CemAcpt::LocalPortAllocator] the local port allocator
|
23
|
+
def use(platform, config, test_data, local_port_allocator)
|
24
|
+
raise Error, "Platform #{platform} is not supported" unless platforms.include?(platform)
|
25
|
+
raise Error, 'test_data must be an Array' unless test_data.is_a?(Array)
|
26
|
+
|
27
|
+
logger.info "Using #{platform} for #{test_data.length} tests..."
|
28
|
+
test_data.each_with_object([]) do |single_test_data, ary|
|
29
|
+
local_port = local_port_allocator.allocate
|
30
|
+
logger.debug("Allocated local port #{local_port} for test #{single_test_data[:test_name]}")
|
31
|
+
ary << new_platform_object(platform, config, single_test_data, local_port)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns an un-initialized platform specific Class of the given platform.
|
36
|
+
# @param platform [String] the name of the platform
|
37
|
+
def get(platform)
|
38
|
+
raise Error, "Platform #{platform} is not supported" unless platforms.include?(platform)
|
39
|
+
|
40
|
+
platform_class(platform)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Dynamically creates a new platform class if it doesn't exist
|
46
|
+
# and returns a new instance of it.
|
47
|
+
# @param platform [String] the name of the platform.
|
48
|
+
# @param config [CemAcpt::Config] the config object.
|
49
|
+
# @param single_test_data [Hash] the test data for a single test.
|
50
|
+
# @return [CemAcpt::Platform::Base] an initialized platform class.
|
51
|
+
def new_platform_object(platform, config, single_test_data, local_port)
|
52
|
+
raise Error, 'single_test_data must be a Hash' unless single_test_data.is_a?(Hash)
|
53
|
+
|
54
|
+
platform_class(platform).new(config, single_test_data, local_port)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Creates a new platform-specific Class object for the given platform.
|
58
|
+
# Does not initialize the class.
|
59
|
+
# @param platform [String] the name of the platform.
|
60
|
+
# @return [Class] the platform-specific Object.
|
61
|
+
def platform_class(platform)
|
62
|
+
# We require the platform base class here so that we can use it as
|
63
|
+
# a parent class for the platform-specific class.
|
64
|
+
require_relative 'platform/base'
|
65
|
+
# If the class has already been defined, we can just use it.
|
66
|
+
if Object.const_defined?(platform.capitalize)
|
67
|
+
klass = Object.const_get(platform.capitalize)
|
68
|
+
else
|
69
|
+
# Otherwise, we need to create the class. We do this by setting
|
70
|
+
# a new constant with the name of the platform capitalized, and
|
71
|
+
# associate that constant with a new instance of Class that inherits
|
72
|
+
# from the platform base class. We then require the platform file,
|
73
|
+
# include and extend our class with the Platform module from the file,
|
74
|
+
# include Logging and Concurrent::Async, and finally call the
|
75
|
+
# initialize method on the class.
|
76
|
+
klass = Object.const_set(
|
77
|
+
platform.capitalize,
|
78
|
+
Class.new(CemAcpt::Platform::Base) do
|
79
|
+
require_relative "platform/#{platform}"
|
80
|
+
include Platform
|
81
|
+
end,
|
82
|
+
)
|
83
|
+
end
|
84
|
+
klass
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns an array of the names of the supported platforms.
|
88
|
+
# Supported platforms are discovered by looking for files in the
|
89
|
+
# platform directory, and platform names are the basename (no extension)
|
90
|
+
# of the files. We deliberately exclude the base class, as it is not
|
91
|
+
# a platform.
|
92
|
+
def platforms
|
93
|
+
return @platforms if defined?(@platforms)
|
94
|
+
|
95
|
+
@platforms = Dir.glob(File.join(PLATFORM_DIR, '*.rb')).map do |file|
|
96
|
+
File.basename(file, '.rb') unless file.end_with?('base.rb')
|
97
|
+
end
|
98
|
+
@platforms.compact!
|
99
|
+
logger.debug "Discovered platform(s): #{@platforms}"
|
100
|
+
@platforms
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'logging'
|
4
|
+
|
5
|
+
module CemAcpt
|
6
|
+
# Holds modules that provide helper methods for various Puppet-related
|
7
|
+
# tasks.
|
8
|
+
module PuppetHelpers
|
9
|
+
# Provides helper methods for Puppet Modules.
|
10
|
+
module Module
|
11
|
+
class << self
|
12
|
+
include CemAcpt::Logging
|
13
|
+
end
|
14
|
+
|
15
|
+
# Builds a Puppet module package.
|
16
|
+
# @param module_dir [String] Path to the module directory. If target_dir
|
17
|
+
# is specified as a relative path, it will be relative to the module dir.
|
18
|
+
# @param target_dir [String] Path to the target directory where the package
|
19
|
+
# will be built. This defaults to the relative path 'pkg/'.
|
20
|
+
# @param should_log [Boolean] Whether or not to log the build process.
|
21
|
+
# @return [String] Path to the built package.
|
22
|
+
def self.build_module_package(module_dir, target_dir = nil, should_log: false)
|
23
|
+
require 'puppet/modulebuilder'
|
24
|
+
|
25
|
+
builder_logger = should_log ? logger : nil
|
26
|
+
builder = Puppet::Modulebuilder::Builder.new(File.expand_path(module_dir), target_dir, builder_logger)
|
27
|
+
|
28
|
+
# Validates module metadata by raising exception if invalid
|
29
|
+
_metadata = builder.metadata
|
30
|
+
logger.debug("Metadata for module #{builder.release_name} is valid")
|
31
|
+
|
32
|
+
# Builds the module package
|
33
|
+
logger.info("Building module package for #{builder.release_name}")
|
34
|
+
builder.build
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|