cem_acpt 0.1.0 → 0.2.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 +4 -4
- data/.gitignore +7 -0
- data/Gemfile.lock +36 -15
- data/README.md +8 -4
- data/cem_acpt.gemspec +10 -10
- data/exe/cem_acpt +29 -3
- data/lib/cem_acpt/context.rb +132 -39
- data/lib/cem_acpt/core_extensions.rb +9 -12
- data/lib/cem_acpt/logging.rb +177 -19
- data/lib/cem_acpt/platform/base/cmd.rb +8 -2
- data/lib/cem_acpt/platform/gcp/cmd.rb +162 -79
- data/lib/cem_acpt/platform/gcp/compute.rb +6 -1
- data/lib/cem_acpt/platform/gcp.rb +3 -3
- data/lib/cem_acpt/puppet_helpers.rb +1 -0
- data/lib/cem_acpt/rspec_utils.rb +242 -0
- data/lib/cem_acpt/shared_objects.rb +147 -26
- data/lib/cem_acpt/spec_helper_acceptance.rb +21 -13
- data/lib/cem_acpt/test_data.rb +3 -14
- data/lib/cem_acpt/test_runner/run_handler.rb +187 -0
- data/lib/cem_acpt/test_runner/runner.rb +228 -0
- data/lib/cem_acpt/test_runner/runner_result.rb +103 -0
- data/lib/cem_acpt/test_runner.rb +10 -0
- data/lib/cem_acpt/utils.rb +84 -12
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +18 -11
- metadata +47 -44
- data/.travis.yml +0 -6
- data/lib/cem_acpt/runner.rb +0 -304
@@ -240,6 +240,9 @@ module CemAcpt::Platform::Gcp
|
|
240
240
|
end
|
241
241
|
|
242
242
|
def create
|
243
|
+
# Add the test ssh key to os-login
|
244
|
+
logger.debug("Adding test SSH key to os-login for #{name}")
|
245
|
+
@cmd.local_exec("compute os-login ssh-keys add --key-file #{@cmd.ssh_key}.pub --project #{project.name} --ttl 4h")
|
243
246
|
@cmd.local_exec(create_cmd)
|
244
247
|
rescue StandardError => e
|
245
248
|
raise "Failed to create VM #{name} with command #{create_cmd}: #{e}"
|
@@ -253,7 +256,7 @@ module CemAcpt::Platform::Gcp
|
|
253
256
|
|
254
257
|
logger.debug("Checking instance #{name} SSH connectivity")
|
255
258
|
@cmd.ssh_ready?(name)
|
256
|
-
rescue StandardError
|
259
|
+
rescue StandardError, Exception
|
257
260
|
false
|
258
261
|
end
|
259
262
|
|
@@ -263,7 +266,9 @@ module CemAcpt::Platform::Gcp
|
|
263
266
|
|
264
267
|
def install_puppet_module_package(module_pkg_path, remote_path, puppet_path)
|
265
268
|
@cmd.scp_upload(@name, module_pkg_path, remote_path)
|
269
|
+
logger.info("Uploaded module package #{module_pkg_path} to #{remote_path} on #{@name}")
|
266
270
|
@cmd.ssh(@name, "sudo #{puppet_path} module install #{remote_path}")
|
271
|
+
logger.info("Installed module package #{remote_path} on #{@name}")
|
267
272
|
end
|
268
273
|
|
269
274
|
private
|
@@ -39,7 +39,7 @@ module Platform
|
|
39
39
|
# If necessary, can pass information into the block to be used in the test suite.
|
40
40
|
def run_tests(&block)
|
41
41
|
logger.debug("Running tests for #{node_name}...")
|
42
|
-
block.call
|
42
|
+
block.call @instance.cmd.env
|
43
43
|
end
|
44
44
|
|
45
45
|
# Uploads and installs a Puppet module package on the GCP instance.
|
@@ -70,7 +70,7 @@ module Platform
|
|
70
70
|
# @param opts [Hash] options to pass to the apply command
|
71
71
|
# @return [String] the output of the apply command
|
72
72
|
def apply_manifest(instance_name, manifest, opts = {})
|
73
|
-
|
73
|
+
command_provider.apply_manifest(instance_name, manifest, opts)
|
74
74
|
end
|
75
75
|
|
76
76
|
# Runs a shell command on the given instance
|
@@ -79,7 +79,7 @@ module Platform
|
|
79
79
|
# @param opts [Hash] options to pass to the run_shell command
|
80
80
|
# @return [String] the output of the run_shell command
|
81
81
|
def run_shell(instance_name, cmd, opts = {})
|
82
|
-
|
82
|
+
command_provider.run_shell(instance_name, cmd, opts)
|
83
83
|
end
|
84
84
|
end
|
85
85
|
end
|
@@ -21,6 +21,7 @@ module CemAcpt
|
|
21
21
|
# @return [String] Path to the built package.
|
22
22
|
def self.build_module_package(module_dir, target_dir = nil, should_log: false)
|
23
23
|
require 'puppet/modulebuilder'
|
24
|
+
require 'fileutils'
|
24
25
|
|
25
26
|
builder_logger = should_log ? logger : nil
|
26
27
|
builder = Puppet::Modulebuilder::Builder.new(File.expand_path(module_dir), target_dir, builder_logger)
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require 'open3'
|
5
|
+
require 'pty'
|
6
|
+
require 'shellwords'
|
7
|
+
require_relative 'logging'
|
8
|
+
|
9
|
+
module CemAcpt
|
10
|
+
# Module that provides methods and objects for running and managing RSpec
|
11
|
+
module RSpecUtils
|
12
|
+
class BundlerNotFoundError < StandardError; end
|
13
|
+
class RSpecNotFoundError < StandardError; end
|
14
|
+
|
15
|
+
# Holds and formats a RSpec command
|
16
|
+
class Command
|
17
|
+
include CemAcpt::LoggingAsync
|
18
|
+
attr_reader :debug, :format, :test_path, :use_bundler, :pty_pid
|
19
|
+
|
20
|
+
# @param opts [Hash] options hash for the RSpec command
|
21
|
+
# @option opts [String] :test_path The path (or glob path) to the test file(s) to run. If blank, runs all.
|
22
|
+
# @option opts [Hash] :format Format options for rspec where the key is the format (documentation, json, etc)
|
23
|
+
# and the value is the out-file path. If you do not want to save the results of a format to a file, the
|
24
|
+
# value should be `nil`.
|
25
|
+
# @option opts [Boolean] :debug True if RSpec should run in debug mode, false if not. Mutually exclusive with
|
26
|
+
# `:quiet`. Default is `false`.
|
27
|
+
# @option opts [Boolean] :quiet True if no output should be logged from RSpec command, false if output should
|
28
|
+
# be logged. Mutually exclusive with `:debug`. Default is `false`.
|
29
|
+
# @option opts [Boolean] :use_bundler Whether or not `bundle exec` should be used to run the RSpec command.
|
30
|
+
# Default is `true`.
|
31
|
+
# @option opts [Boolean] :bundle_install Whether or not to run `bundle install` before the RSpec command
|
32
|
+
# if `use_bundler` is `true`.
|
33
|
+
# @option opts [Boolean] :use_shell Whether or not to add `$SHELL` as a prefix to the command
|
34
|
+
# @option opts [Hash] :env Environment variables to prepend to the command
|
35
|
+
def initialize(opts = {})
|
36
|
+
@test_path = opts[:test_path]&.shellescape
|
37
|
+
@format = opts.fetch(:format, {})
|
38
|
+
@debug = opts.fetch(:debug, false)
|
39
|
+
@quiet = @debug ? false : opts.fetch(:quiet, false)
|
40
|
+
@use_bundler = opts.fetch(:use_bundler, false)
|
41
|
+
@bundle_install = opts.fetch(:bundle_install, false)
|
42
|
+
@env = opts.fetch(:env, {})
|
43
|
+
@pty_pid = nil
|
44
|
+
validate_and_set_bin_paths(opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Sets debug mode to `true`
|
48
|
+
def set_debug
|
49
|
+
@debug = true
|
50
|
+
if @quiet
|
51
|
+
async_debug('Setting :quiet to false because :debug is now true.')
|
52
|
+
@quiet = false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sets debug mode to `false`
|
57
|
+
def unset_debug
|
58
|
+
@debug = false
|
59
|
+
end
|
60
|
+
|
61
|
+
def quiet
|
62
|
+
@quiet && !debug
|
63
|
+
end
|
64
|
+
|
65
|
+
# Adds a new format to the RSpec command
|
66
|
+
# @param fmt [String] The name of the format (i.e. "documentation", "json", etc.)
|
67
|
+
# @param out [String] If specified, saves the specified format to a file at this path
|
68
|
+
def with_format(fmt, out: nil)
|
69
|
+
@format[fmt.to_sym] = out
|
70
|
+
end
|
71
|
+
|
72
|
+
# Environment variables that will be used for the RSpec command
|
73
|
+
# @return [Hash] A Hash of environment variables with each key pair being: <var name> => <var value>
|
74
|
+
def env
|
75
|
+
@debug ? @env.merge({ 'RSPEC_DEBUG' => 'true' }) : @env
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns an array representation of the RSpec command
|
79
|
+
def to_a
|
80
|
+
cmd = cmd_base.dup
|
81
|
+
cmd << test_path if test_path
|
82
|
+
format.each do |fmt, out|
|
83
|
+
cmd += ['--format', fmt.to_s.shellescape]
|
84
|
+
cmd += ['--out', out.to_s.shellescape] if out
|
85
|
+
end
|
86
|
+
cmd.compact
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns a string representation of the RSpec command
|
90
|
+
def to_s
|
91
|
+
to_a.join(' ')
|
92
|
+
end
|
93
|
+
|
94
|
+
# Executes the RSpec command on the current machine
|
95
|
+
# @param pty [Boolean] If true, execute command in a PTY. If false, execute command directly.
|
96
|
+
# @param log_prefix [String] A prefix to add to log messages generated while the command is running.
|
97
|
+
def execute(pty: true, log_prefix: 'RSPEC')
|
98
|
+
if pty
|
99
|
+
execute_pty(log_prefix: log_prefix)
|
100
|
+
else
|
101
|
+
execute_no_pty(log_prefix: log_prefix)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Executes the RSpec command in a psuedo-terminal (PTY). First, it spawns a process
|
106
|
+
# for $SHELL, sets environment variables `export_envs`, then calls the current RSpec
|
107
|
+
# command in the shell and exits with the last exit code `$?`. Output is read from the
|
108
|
+
# RSpec command in near real-time in a blocking manner unless the `:quiet` option has
|
109
|
+
# been specified.
|
110
|
+
# @param log_prefix [String] A prefix to add to the log messages that are output from
|
111
|
+
# the RSpec command.
|
112
|
+
# @return [Integer] The exit code of the RSpec command
|
113
|
+
def execute_pty(log_prefix: 'RSPEC')
|
114
|
+
async_debug("Executing RSpec command '#{self}' in PTY...", log_prefix)
|
115
|
+
PTY.spawn(env, ENV['SHELL']) do |r, w, pid|
|
116
|
+
@pty_pid = pid
|
117
|
+
async_debug("Spawned RSpec PTY with PID #{@pty_pid}", log_prefix)
|
118
|
+
export_envs(w)
|
119
|
+
w.puts "#{self}; exit $?"
|
120
|
+
quiet ? wait_io(r) : read_io(r, log_prefix: log_prefix)
|
121
|
+
end
|
122
|
+
$CHILD_STATUS
|
123
|
+
end
|
124
|
+
|
125
|
+
# Executes the RSpec command using Open3.popen2e(). The output stream, which is both
|
126
|
+
# stderr and stdout, is read in real-time in a non-blocking manner.
|
127
|
+
# @param log_prefix [String] A prefix to add to the log messages that are output from
|
128
|
+
# the RSpec command.
|
129
|
+
# @return [Integer] The exit code of the RSpec command
|
130
|
+
def execute_no_pty(log_prefix: 'RSPEC')
|
131
|
+
async_info("Executing RSpec command '#{self}' with Open3.popen2e()...", log_prefix)
|
132
|
+
exit_status = nil
|
133
|
+
Open3.popen2e(env, to_s) do |stdin, std_out_err, wait_thr|
|
134
|
+
stdin.close
|
135
|
+
quiet ? wait_io(std_out_err) : read_io(std_out_err, log_prefix: log_prefix)
|
136
|
+
exit_status = wait_thr.value
|
137
|
+
end
|
138
|
+
exit_status
|
139
|
+
end
|
140
|
+
|
141
|
+
# Kills the PTY process with `SIGKILL` if the process exists
|
142
|
+
def kill_pty
|
143
|
+
Process.kill('KILL', @pty_pid) unless @pty_pid.nil?
|
144
|
+
rescue Errno::ESRCH
|
145
|
+
true
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# Detects if the current Ruby context is JRuby
|
151
|
+
def jruby?
|
152
|
+
File.basename(RbConfig.ruby) == 'jruby'
|
153
|
+
end
|
154
|
+
|
155
|
+
# The base RSpec command
|
156
|
+
def cmd_base
|
157
|
+
use_bundler ? cmd_base_bundler : cmd_base_rspec
|
158
|
+
end
|
159
|
+
|
160
|
+
# The base RSpec command if `:use_bundler` is `true`.
|
161
|
+
def cmd_base_bundler
|
162
|
+
base = [@bundle, 'exec', 'rspec']
|
163
|
+
base.unshift("#{@bundle} install;") if @bundle_install
|
164
|
+
base
|
165
|
+
end
|
166
|
+
|
167
|
+
# The base RSpec command if `:use_bundler` is `false`
|
168
|
+
def cmd_base_rspec
|
169
|
+
[@rspec]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Puts export statements for each key-value pair in `env` to the given writer.
|
173
|
+
# Writer is the write pipe of a PTY session, or a similar IO object that can
|
174
|
+
# pass the statements to a shell.
|
175
|
+
# @param writer [IO] An IO object that supprts `puts` and can send statements to a shell
|
176
|
+
def export_envs(writer)
|
177
|
+
env.each do |key, val|
|
178
|
+
writer.puts "export #{key}=#{val}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Finds and sets the paths to the `bundle` and `rspec` binaries. The paths can
|
183
|
+
# be either passed in as options in the `opts` Hash or interrogated from the
|
184
|
+
# system.
|
185
|
+
# @param opts [Hash] The options hash
|
186
|
+
# @option opts [String] :bundle An absolute path on the system to the `bundle` binary.
|
187
|
+
# @option opts [String] :rspec An absolute path on the system to the `rspec` binary.
|
188
|
+
# @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
|
189
|
+
# `bundle` binary is not found.
|
190
|
+
# @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
|
191
|
+
def validate_and_set_bin_paths(opts = {})
|
192
|
+
%i[bundle rspec].each do |bin|
|
193
|
+
bin_path = opts[bin] || `command -v #{bin}`.strip
|
194
|
+
bin_not_found(bin, bin_path) unless bin_path && File.exist?(bin_path)
|
195
|
+
instance_variable_set("@#{bin}", bin_path)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Handles binary paths which are not found
|
200
|
+
# @param bin [Symbol] The binary that was not found, either :bundle or :rspec.
|
201
|
+
# @param bin_path [String] The path to the binary that was checked.
|
202
|
+
# @raise [CemAcpt::RSpecUtils::BundlerNotFoundError] if `@use_bundler` is true and
|
203
|
+
# `bundle` binary is not found.
|
204
|
+
# @raise [CemAcpt::RSpecUtils::RSpecNotFoundError] if `rspec` binary is not found.
|
205
|
+
# @raise [RuntimeError] if `bin` is not :bundle or :rspec.
|
206
|
+
def bin_not_found(bin, bin_path)
|
207
|
+
msg_base = "#{bin} not found."
|
208
|
+
msg = bin_path.nil? ? "#{msg_base} Path is nil." : "#{msg_base} Path: #{bin_path}"
|
209
|
+
case bin
|
210
|
+
when :bundle
|
211
|
+
raise BundlerNotFoundError, msg if @use_bundler
|
212
|
+
when :rspec
|
213
|
+
raise RSpecNotFoundError, msg
|
214
|
+
else
|
215
|
+
raise "bin #{bin} not recognized!"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Blocking wait on an IO stream. Wait stops once the IO stream has reached
|
220
|
+
# end of file.
|
221
|
+
# @param stdout [IO] An IO stream with output that can be read from.
|
222
|
+
def wait_io(stdout)
|
223
|
+
sleep(0.01) until stdout.eof?
|
224
|
+
end
|
225
|
+
|
226
|
+
# Reads and logs data from `stdout` in a near real-time, blocking manner.
|
227
|
+
# @param stdout [IO] An IO stream with output that can be read from.
|
228
|
+
# @param log_prefix [String] A string prefix that is added to log messages.
|
229
|
+
def read_io(stdout, log_prefix: 'RSPEC')
|
230
|
+
loop do
|
231
|
+
chunk = stdout.read_nonblock(4096).strip
|
232
|
+
async_info(chunk, log_prefix) unless chunk.nil? || chunk.empty?
|
233
|
+
rescue IO::WaitReadable
|
234
|
+
IO.select([stdout])
|
235
|
+
retry
|
236
|
+
rescue EOFError
|
237
|
+
break
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'concurrent-ruby'
|
4
4
|
require 'deep_merge'
|
5
5
|
require 'json'
|
6
|
-
require 'ostruct'
|
7
6
|
require 'yaml'
|
8
7
|
require_relative 'core_extensions'
|
9
8
|
require_relative 'logging'
|
@@ -49,7 +48,16 @@ module CemAcpt
|
|
49
48
|
attr_reader :config_file
|
50
49
|
|
51
50
|
def initialize
|
52
|
-
@opts = {
|
51
|
+
@opts = {
|
52
|
+
thread_pool: {
|
53
|
+
min_threads: [2, Concurrent.processor_count - 1].max,
|
54
|
+
max_threads: [2, Concurrent.processor_count - 1].max,
|
55
|
+
max_queue: [2, Concurrent.processor_count - 1].max * 10,
|
56
|
+
fallback_policy: :caller_runs,
|
57
|
+
idletime: 1200,
|
58
|
+
auto_terminate: true,
|
59
|
+
},
|
60
|
+
}
|
53
61
|
end
|
54
62
|
|
55
63
|
# Returns the value of the dot-separated key.
|
@@ -67,18 +75,25 @@ module CemAcpt
|
|
67
75
|
!!get(dot_key)
|
68
76
|
end
|
69
77
|
|
78
|
+
# Checks to see if cem_acpt is set to run in debug mode (log level is set to debug)
|
79
|
+
# @return [TrueClass] If cem_acpt is running in debug mode
|
80
|
+
# @return [FalseClass] If cem_acpt is not runnint in debug mode
|
81
|
+
def debug_mode?
|
82
|
+
@opts.dot_dig('log_level') == 'debug'
|
83
|
+
end
|
84
|
+
|
70
85
|
# Loads the config from specified file path. Config files must be in YAML
|
71
86
|
# format. If 'opts' is specified, it will be merged with the config file.
|
72
87
|
# Values in 'opts' will override values in the config file.
|
73
88
|
# @param opts [Hash] Options to be merged with the config file
|
74
89
|
# @param config_file [String] Path to the config file
|
75
|
-
def load(opts: {}, config_file: '
|
90
|
+
def load(opts: {}, config_file: './cem_acpt_config.yaml')
|
76
91
|
raise ConfigImmutableError, 'Config is immutable, cannot load more than once' if frozen?
|
77
92
|
raise ArgumentError, 'opts must be a Hash' unless opts.is_a?(Hash)
|
78
93
|
raise ArgumentError, 'config_file must be a String' unless config_file.is_a?(String)
|
79
94
|
|
80
95
|
@config_file = File.expand_path(config_file)
|
81
|
-
@opts
|
96
|
+
@opts.deep_merge!(load_opts_from_file(File.expand_path(config_file)).deep_merge!(opts))
|
82
97
|
@opts.format!
|
83
98
|
@opts.freeze
|
84
99
|
freeze
|
@@ -155,8 +170,13 @@ module CemAcpt
|
|
155
170
|
end
|
156
171
|
end
|
157
172
|
|
173
|
+
class NodeInventoryFileNotFoundError < StandardError; end
|
174
|
+
class NodeInventoryFileLoadError < StandardError; end
|
158
175
|
class NodeClaimedError < StandardError; end
|
159
176
|
class NodeDoesNotExistError < StandardError; end
|
177
|
+
class PropertyNotFoundError < StandardError; end
|
178
|
+
class LockWaitTimeoutError < StandardError; end
|
179
|
+
class LockNotRecognizedError < StandardError; end
|
160
180
|
|
161
181
|
# Provides a thread-safe inventory of test nodes.
|
162
182
|
class NodeInventory
|
@@ -169,7 +189,8 @@ module CemAcpt
|
|
169
189
|
@lock = Concurrent::ReadWriteLock.new
|
170
190
|
@claimed = Concurrent::Set.new
|
171
191
|
@save_on_claim = false
|
172
|
-
@save_file_path = 'spec/fixtures/node_inventory
|
192
|
+
@save_file_path = 'spec/fixtures/node_inventory'
|
193
|
+
@loaded_node_inv = nil
|
173
194
|
end
|
174
195
|
|
175
196
|
# When called, enables saving the inventory to a file on claim.
|
@@ -189,12 +210,51 @@ module CemAcpt
|
|
189
210
|
@inventory.put_if_absent(node_name, node_data)
|
190
211
|
end
|
191
212
|
|
213
|
+
def update(node_name, node_data)
|
214
|
+
@inventory.replace_if_exists(node_name, node_data)
|
215
|
+
end
|
216
|
+
|
192
217
|
# Returns the node data for a given node.
|
193
218
|
# @param node_name [String] The name of the node to get data for.
|
194
219
|
def get(node_name)
|
195
220
|
@inventory[node_name]
|
196
221
|
end
|
197
222
|
|
223
|
+
# Returns the node_name and node_data for a given node
|
224
|
+
# that has a matching value for the given property.
|
225
|
+
# @param property_path [String] The dot-separated property path to check.
|
226
|
+
# @param value [Object] The value to check for.
|
227
|
+
# @return [Array] Two-item frozen array of node name and node data.
|
228
|
+
# @raise [NodeDoesNotExistError] If no nodes are found with the property equal to the value.
|
229
|
+
def get_by_property(property_path, value)
|
230
|
+
found = []
|
231
|
+
@inventory.each_pair do |node_name, node_data|
|
232
|
+
next unless node_data.dot_dig(property_path) == value
|
233
|
+
|
234
|
+
found = [node_name, node_data].freeze
|
235
|
+
end
|
236
|
+
raise NodeDoesNotExistError, "No node found with property #{property_path} == #{value}" if found.empty?
|
237
|
+
|
238
|
+
found
|
239
|
+
end
|
240
|
+
|
241
|
+
# Returns all property values of all nodes for the given property path
|
242
|
+
# @param property_path [String] The dot-separated property path to check.
|
243
|
+
# @return [Array] A frozen array of found property values.
|
244
|
+
# @raise [PropertyNotFoundError] If no values are found on any nodes for the property path.
|
245
|
+
def get_all_properties(property_path)
|
246
|
+
found = []
|
247
|
+
@inventory.each_pair do |_, node_data|
|
248
|
+
prop = node_data.dot_dig(property_path)
|
249
|
+
next unless prop
|
250
|
+
|
251
|
+
found << prop
|
252
|
+
end
|
253
|
+
raise PropertyNotFoundError, "No property with path #{property_path} found in nodes" if found.empty?
|
254
|
+
|
255
|
+
found.freeze
|
256
|
+
end
|
257
|
+
|
198
258
|
# Sets a specific property on a node in the inventory.
|
199
259
|
# @param node_name [String] The name of the node to set the property on.
|
200
260
|
# @param property_path [String] The dot-separated property path to set.
|
@@ -230,7 +290,7 @@ module CemAcpt
|
|
230
290
|
# @raise [NodeDoesNotExistError] If the node does not exist in the inventory.
|
231
291
|
# @raise [NodeClaimedError] If the node is already claimed.
|
232
292
|
def claim(node_name)
|
233
|
-
|
293
|
+
with_lock_retry(:write) do
|
234
294
|
unless @inventory.keys.include?(node_name)
|
235
295
|
raise NodeDoesNotExistError, "Node #{node_name} does not exist in inventory"
|
236
296
|
end
|
@@ -254,16 +314,24 @@ module CemAcpt
|
|
254
314
|
# @return [String] The name of the node that was claimed.
|
255
315
|
# @raise [NodeDoesNotExistError] If no valid node is found.
|
256
316
|
def claim_by_property(property_path, value)
|
257
|
-
|
317
|
+
attempts ||= 1
|
258
318
|
claim_name = nil
|
259
319
|
@inventory.each_pair do |node_name, node_data|
|
260
|
-
next if
|
320
|
+
next if @claimed.include?(node_name)
|
261
321
|
|
262
322
|
claim_name = node_name if node_data.dot_dig(property_path) == value
|
263
323
|
end
|
264
|
-
|
265
|
-
|
266
|
-
|
324
|
+
raise NodeDoesNotExistError, "No node found with property #{property_path} == #{value}" if claim_name.nil?
|
325
|
+
|
326
|
+
claim(claim_name)
|
327
|
+
rescue NodeDoesNotExistError => e
|
328
|
+
# We sleep than retry three times to help mitigate race conditions
|
329
|
+
if (attempts += 1) <= 3
|
330
|
+
sleep(1)
|
331
|
+
retry
|
332
|
+
else
|
333
|
+
raise e
|
334
|
+
end
|
267
335
|
end
|
268
336
|
|
269
337
|
# Deletes a node from the inventory.
|
@@ -286,7 +354,7 @@ module CemAcpt
|
|
286
354
|
|
287
355
|
# Clears the inventory and removes claimed nodes. Thread-safe.
|
288
356
|
def clear!
|
289
|
-
|
357
|
+
with_lock_retry(:write) do
|
290
358
|
clear_no_lock!
|
291
359
|
end
|
292
360
|
end
|
@@ -308,6 +376,16 @@ module CemAcpt
|
|
308
376
|
h
|
309
377
|
end
|
310
378
|
|
379
|
+
# Returns a YAML string of a specific node's node data
|
380
|
+
def node_to_yaml(node_name)
|
381
|
+
get(node_name).to_h.to_yaml
|
382
|
+
end
|
383
|
+
|
384
|
+
# Returns a JSON string of a specific node's node data
|
385
|
+
def node_to_json(node_name, *args)
|
386
|
+
get(node_name).to_h.to_json(*args)
|
387
|
+
end
|
388
|
+
|
311
389
|
# Returns a YAML string of the inventory.
|
312
390
|
def to_yaml
|
313
391
|
to_h.to_yaml
|
@@ -321,15 +399,21 @@ module CemAcpt
|
|
321
399
|
# Saves the current node inventory to a file. DOES NOT USE LOCK.
|
322
400
|
# If this is called outside of a lock, it will not be thread-safe.
|
323
401
|
def save_no_lock!(file_path = @save_file_path)
|
324
|
-
|
325
|
-
|
402
|
+
Dir.mkdir(file_path) unless Dir.exist?(file_path)
|
403
|
+
@inventory.each_pair do |node_name, node_data|
|
404
|
+
path = File.join(file_path, "#{node_name}.yaml")
|
405
|
+
next if File.file?(path)
|
406
|
+
|
407
|
+
File.open(path, 'w') do |f|
|
408
|
+
f.write(node_data.to_h.to_yaml)
|
409
|
+
end
|
326
410
|
end
|
327
411
|
end
|
328
412
|
|
329
413
|
# Saves the current node inventory to a yaml file. Thread-safe.
|
330
414
|
# @param file_path [String] The path to the file to save to.
|
331
415
|
def save(file_path = @save_file_path)
|
332
|
-
|
416
|
+
with_lock_retry(:write) do
|
333
417
|
save_no_lock!(file_path)
|
334
418
|
end
|
335
419
|
end
|
@@ -338,23 +422,58 @@ module CemAcpt
|
|
338
422
|
# If this is called outside of a lock, it will not be thread-safe.
|
339
423
|
def load_no_lock!(file_path = @save_file_path)
|
340
424
|
require 'net/ssh/proxy/command' # If ProxyCommand is used in ssh options, this is required.
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
add(node_name, node_data)
|
347
|
-
end
|
425
|
+
attempts ||= 1
|
426
|
+
Dir.glob('*.yaml', base: file_path) do |file_name|
|
427
|
+
node_name = File.basename(file_name, '.yaml')
|
428
|
+
node_data = YAML.load_file(File.join(file_path, file_name))
|
429
|
+
add(node_name, node_data) || update(node_name, node_data)
|
348
430
|
end
|
431
|
+
rescue StandardError => e
|
432
|
+
raise e unless (attempts += 1) <= 3
|
433
|
+
|
434
|
+
sleep(1)
|
435
|
+
retry
|
349
436
|
end
|
350
437
|
|
438
|
+
def update_no_lock!; end
|
439
|
+
|
351
440
|
# Loads a node inventory from a yaml file. Thread-safe.
|
352
441
|
# @param file_path [String] The path to the file to load from.
|
353
442
|
def load(file_path = @save_file_path)
|
354
|
-
|
443
|
+
with_lock_retry(:write) do
|
355
444
|
load_no_lock!(file_path)
|
356
445
|
end
|
357
446
|
end
|
447
|
+
|
448
|
+
def clean_local_files(file_path = @save_file_path)
|
449
|
+
Dir.glob('*.yaml', base: file_path) do |file_name|
|
450
|
+
File.delete(File.expand_path(File.join(file_path, file_name)))
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
private
|
455
|
+
|
456
|
+
def with_lock_retry(lock_type, max_attempts: 3)
|
457
|
+
raise 'No block given' unless block_given?
|
458
|
+
|
459
|
+
attempts ||= 1
|
460
|
+
case lock_type
|
461
|
+
when :read
|
462
|
+
@lock.with_read_lock { yield }
|
463
|
+
when :write
|
464
|
+
@lock.with_write_lock { yield }
|
465
|
+
else
|
466
|
+
raise LockNotRecognizedError, "Lock type #{lock_type} not recognized! Must be :read or :write!"
|
467
|
+
end
|
468
|
+
rescue Concurrent::ResourceLimitError => e
|
469
|
+
if (attempts += 1) <= max_attempts
|
470
|
+
sleep(1)
|
471
|
+
logger.debug("Failed to acquire lock with error #{e.message}. Retrying...")
|
472
|
+
retry
|
473
|
+
else
|
474
|
+
raise LockWaitTimeoutError, 'Lock could not be aquired'
|
475
|
+
end
|
476
|
+
end
|
358
477
|
end
|
359
478
|
|
360
479
|
class LocalPortAllocationError < StandardError; end
|
@@ -391,9 +510,11 @@ module CemAcpt
|
|
391
510
|
|
392
511
|
ports << port
|
393
512
|
end
|
394
|
-
|
395
|
-
|
396
|
-
|
513
|
+
if ports.length == 1
|
514
|
+
ports[0]
|
515
|
+
else
|
516
|
+
ports
|
517
|
+
end
|
397
518
|
end
|
398
519
|
|
399
520
|
private
|
@@ -14,15 +14,12 @@ module CemAcpt
|
|
14
14
|
config.extend CemAcpt::SpecHelperAcceptance
|
15
15
|
config.add_setting :acpt_test_data, default: {}
|
16
16
|
config.add_setting :acpt_node_inventory
|
17
|
+
config.threadsafe = true
|
18
|
+
config.color_mode = :off
|
19
|
+
config.fail_fast = false
|
17
20
|
end
|
18
21
|
|
19
|
-
return unless RSpec.configuration.acpt_node_inventory.nil?
|
20
|
-
|
21
|
-
raise 'Node inventory file not found!' unless File.exist?('spec/fixtures/node_inventory.yaml')
|
22
|
-
|
23
22
|
node_inventory = NodeInventory.new
|
24
|
-
node_inventory.save_on_claim
|
25
|
-
node_inventory.save_file_path = 'spec/fixtures/node_inventory.yaml'
|
26
23
|
node_inventory.load
|
27
24
|
RSpec.configuration.acpt_node_inventory = node_inventory
|
28
25
|
end
|
@@ -35,8 +32,10 @@ module CemAcpt
|
|
35
32
|
def initialize_test_environment!
|
36
33
|
test_file = caller_locations.first.path
|
37
34
|
|
38
|
-
node_name = RSpec.configuration.acpt_node_inventory.
|
39
|
-
|
35
|
+
node_name, node_data = RSpec.configuration.acpt_node_inventory.get_by_property('test_data.test_file', test_file)
|
36
|
+
raise "Failed to get node data for node #{node_name}" unless node_data
|
37
|
+
raise "Node data format is incorrect: #{node_data}" unless node_data.is_a?(Hash)
|
38
|
+
|
40
39
|
backend = nil
|
41
40
|
host = nil
|
42
41
|
ssh_options = nil
|
@@ -81,13 +80,14 @@ module CemAcpt
|
|
81
80
|
RSpec.configuration.acpt_test_data = acpt_test_data
|
82
81
|
end
|
83
82
|
|
84
|
-
# This method formats Puppet Apply options
|
83
|
+
# This method formats Puppet Apply options
|
85
84
|
def puppet_apply_options(opts = {})
|
86
85
|
if [opts[:catch_changes], opts[:expect_changes], opts[:catch_failures], opts[:expect_failures]].compact.length > 1
|
87
|
-
raise ArgumentError,
|
86
|
+
raise ArgumentError,
|
87
|
+
'Please specify only one of "catch_changes", "expect_changes", "catch_failures", or "expect_failures"'
|
88
88
|
end
|
89
89
|
|
90
|
-
apply_opts = {
|
90
|
+
apply_opts = {}.merge(opts)
|
91
91
|
|
92
92
|
if opts[:catch_changes]
|
93
93
|
apply_opts[:detailed_exit_codes] = true
|
@@ -169,8 +169,16 @@ module CemAcpt
|
|
169
169
|
# asserts that the second run has no changes.
|
170
170
|
def idempotent_apply(manifest, opts = {})
|
171
171
|
opts.reject! { |k, _| %i[catch_changes expect_changes catch_failures expect_failures].include?(k) }
|
172
|
-
|
173
|
-
|
172
|
+
begin
|
173
|
+
apply_manifest(manifest, opts.merge({ catch_failures: true }))
|
174
|
+
rescue StandardError => e
|
175
|
+
raise "Idempotent apply failed during first apply: #{e.message}\n#{e.backtrace.join("\n")}"
|
176
|
+
end
|
177
|
+
begin
|
178
|
+
apply_manifest(manifest, opts.merge({ catch_changes: true }))
|
179
|
+
rescue StandardError => e
|
180
|
+
raise "Idempotent apply failed during second apply: #{e.message}\n#{e.backtrace.join("\n")}"
|
181
|
+
end
|
174
182
|
end
|
175
183
|
end
|
176
184
|
end
|