cem_acpt 0.2.5 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +38 -0
- data/Gemfile +4 -3
- data/Gemfile.lock +85 -56
- data/README.md +144 -83
- data/cem_acpt.gemspec +8 -7
- data/exe/cem_acpt +41 -7
- data/lib/cem_acpt/config.rb +345 -0
- data/lib/cem_acpt/core_extensions.rb +17 -61
- data/lib/cem_acpt/goss/api/action_response.rb +175 -0
- data/lib/cem_acpt/goss/api.rb +83 -0
- data/lib/cem_acpt/goss.rb +8 -0
- data/lib/cem_acpt/image_name_builder.rb +0 -9
- data/lib/cem_acpt/logging/formatter.rb +97 -0
- data/lib/cem_acpt/logging.rb +168 -142
- data/lib/cem_acpt/platform/base.rb +26 -37
- data/lib/cem_acpt/platform/gcp.rb +48 -62
- data/lib/cem_acpt/platform.rb +30 -28
- data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
- data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
- data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
- data/lib/cem_acpt/provision/terraform.rb +193 -0
- data/lib/cem_acpt/provision.rb +20 -0
- data/lib/cem_acpt/puppet_helpers.rb +0 -1
- data/lib/cem_acpt/test_data.rb +23 -13
- data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
- data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
- data/lib/cem_acpt/test_runner.rb +170 -3
- data/lib/cem_acpt/utils/puppet.rb +29 -0
- data/lib/cem_acpt/utils/ssh.rb +197 -0
- data/lib/cem_acpt/utils/terminal.rb +27 -0
- data/lib/cem_acpt/utils.rb +4 -138
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +70 -20
- data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
- data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
- data/lib/terraform/gcp/linux/main.tf +191 -0
- data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
- data/lib/terraform/gcp/windows/.keep +0 -0
- data/sample_config.yaml +22 -21
- metadata +88 -56
- data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
- data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
- data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
- data/lib/cem_acpt/bootstrap.rb +0 -12
- data/lib/cem_acpt/context.rb +0 -153
- data/lib/cem_acpt/platform/base/cmd.rb +0 -71
- data/lib/cem_acpt/platform/gcp/cmd.rb +0 -345
- data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
- data/lib/cem_acpt/platform/vmpooler.rb +0 -24
- data/lib/cem_acpt/rspec_utils.rb +0 -242
- data/lib/cem_acpt/shared_objects.rb +0 -537
- data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
- data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
- data/lib/cem_acpt/test_runner/runner.rb +0 -210
- data/lib/cem_acpt/test_runner/runner_result.rb +0 -103
data/lib/cem_acpt/test_data.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'pathname'
|
3
4
|
require_relative 'core_extensions'
|
4
5
|
require_relative 'logging'
|
5
6
|
|
@@ -19,28 +20,38 @@ module CemAcpt
|
|
19
20
|
include CemAcpt::Logging
|
20
21
|
using CemAcpt::CoreExtensions::ExtendedHash
|
21
22
|
|
22
|
-
attr_reader :
|
23
|
+
attr_reader :acpt_test_dir, :acceptance_tests
|
23
24
|
|
24
25
|
# Initializes a new Fetcher object.
|
25
26
|
# @param config [CemAcpt::Config] the config object
|
26
27
|
def initialize(config)
|
27
28
|
@config = config
|
28
|
-
@
|
29
|
-
|
29
|
+
@acpt_test_dir = Pathname(File.join(@config.get('module_dir'), 'spec', 'acceptance'))
|
30
|
+
find_acceptance_tests!
|
30
31
|
end
|
31
32
|
|
32
33
|
# Extracts, formats, and returns a test data hash.
|
33
34
|
# @return [Array<Hash>] an array of test data hashes
|
34
35
|
def acceptance_test_data
|
35
36
|
logger.info 'Gathering acceptance test data...'
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
raise "No 'tests' entry found in config" unless @config.has?('tests')
|
38
|
+
|
39
|
+
@config.get('tests').each_with_object([]) do |test_name, a|
|
40
|
+
test_dir = acceptance_tests.find { |f| File.basename(f) == test_name }
|
41
|
+
raise "Test directory not found for test #{test_name}" unless test_dir
|
42
|
+
|
43
|
+
goss_file = File.expand_path(File.join(test_dir, 'goss.yaml'))
|
44
|
+
puppet_manifest = File.expand_path(File.join(test_dir, 'manifest.pp'))
|
45
|
+
raise "Goss file not found for test #{test_name}" unless File.exist?(goss_file)
|
46
|
+
raise "Puppet manifest not found for test #{test_name}" unless File.exist?(puppet_manifest)
|
47
|
+
|
48
|
+
logger.debug("Complete test directory found for test #{test_name}: #{test_dir}")
|
39
49
|
test_data = {
|
40
50
|
test_name: test_name,
|
41
|
-
|
51
|
+
test_dir: File.expand_path(test_dir),
|
52
|
+
goss_file: goss_file,
|
53
|
+
puppet_manifest: puppet_manifest,
|
42
54
|
}
|
43
|
-
next unless @config.has?('tests') && @config.get('tests').include?(test_name)
|
44
55
|
|
45
56
|
process_for_each(test_data).each do |test_data_i|
|
46
57
|
process_static_vars(test_data_i)
|
@@ -56,12 +67,11 @@ module CemAcpt
|
|
56
67
|
|
57
68
|
# Locates acceptance tests in the module directory.
|
58
69
|
# @return [Array<String>] the list of acceptance test paths
|
59
|
-
def find_acceptance_tests
|
60
|
-
|
61
|
-
raise 'No acceptance tests found' if
|
70
|
+
def find_acceptance_tests!
|
71
|
+
@acceptance_tests = acpt_test_dir.children.select { |f| f.directory? && File.exist?(File.join(f, 'goss.yaml')) }.map(&:to_s)
|
72
|
+
raise 'No acceptance tests found' if @acceptance_tests.empty?
|
62
73
|
|
63
|
-
logger.info "Found #{
|
64
|
-
tests
|
74
|
+
logger.info "Found #{@acceptance_tests.size} acceptance tests"
|
65
75
|
end
|
66
76
|
|
67
77
|
# Processes a for_each statement in the test data config.
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CemAcpt
|
4
|
+
module TestRunner
|
5
|
+
module LogFormatter
|
6
|
+
# Formats the results of a Goss action
|
7
|
+
class GossActionResponse
|
8
|
+
INDENT = ' '
|
9
|
+
|
10
|
+
def initialize(config, instance_names_ips)
|
11
|
+
@config = config
|
12
|
+
@instance_names_ips = instance_names_ips
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)}>"
|
21
|
+
end
|
22
|
+
|
23
|
+
def summary(response)
|
24
|
+
new_summary_message(response)
|
25
|
+
end
|
26
|
+
|
27
|
+
def results(response)
|
28
|
+
new_results_message(response.results)
|
29
|
+
end
|
30
|
+
|
31
|
+
def host_name(response)
|
32
|
+
name_from_ip(response.host)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_name(response)
|
36
|
+
test_from_ip(response.host)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def normalize_whitespace(str)
|
42
|
+
raise ArgumentError, 'str must be a String' unless str.is_a?(String)
|
43
|
+
|
44
|
+
str.gsub(%r{(\n|\r|\t)}, ' ').gsub(%r{\s{2,}}, ' ').strip
|
45
|
+
end
|
46
|
+
|
47
|
+
def success_str(success)
|
48
|
+
success ? 'passed' : 'failed'
|
49
|
+
end
|
50
|
+
|
51
|
+
def name_from_ip(ip)
|
52
|
+
@instance_names_ips.each do |name, val|
|
53
|
+
return name if val['ip'] == ip
|
54
|
+
end
|
55
|
+
ip
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_from_ip(ip)
|
59
|
+
@instance_names_ips.each do |_name, val|
|
60
|
+
return val['test_name'] if val['ip'] == ip
|
61
|
+
end
|
62
|
+
'<unknown>'
|
63
|
+
end
|
64
|
+
|
65
|
+
def new_summary_message(response)
|
66
|
+
msg = [
|
67
|
+
"SUMMARY: #{success_str(response.success?).capitalize}: Test #{test_from_ip(response.host)}:",
|
68
|
+
"#{normalize_whitespace(response.summary.summary_line)}:",
|
69
|
+
"Action '#{response.action}' on host #{name_from_ip(response.host)}",
|
70
|
+
].join(' ')
|
71
|
+
return msg unless @config.debug?
|
72
|
+
|
73
|
+
[
|
74
|
+
msg,
|
75
|
+
"HTTP Status: #{response.http_status}",
|
76
|
+
"Failed percentage: #{response.summary.failed_percentage}",
|
77
|
+
].join("\n#{INDENT}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def new_results_message(results)
|
81
|
+
results.map { |r| new_result_message(r) }
|
82
|
+
end
|
83
|
+
|
84
|
+
def new_result_message(result)
|
85
|
+
return "Error: #{result.error}" unless result.err.nil?
|
86
|
+
|
87
|
+
status = result.skipped? ? 'Skipped' : success_str(result.success?).capitalize
|
88
|
+
msg = [
|
89
|
+
"#{status}:",
|
90
|
+
normalize_whitespace(result.summary_line),
|
91
|
+
]
|
92
|
+
return msg.join(' ') unless @config.debug?
|
93
|
+
|
94
|
+
[
|
95
|
+
msg.join(' '),
|
96
|
+
"Duration: #{result.duration.to_s}",
|
97
|
+
"Expected: #{result.expected}",
|
98
|
+
"Found: #{result.found}",
|
99
|
+
].join("\n#{INDENT}")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/cem_acpt/test_runner.rb
CHANGED
@@ -1,10 +1,177 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'core_extensions'
|
4
|
+
require_relative 'goss'
|
5
|
+
require_relative 'logging'
|
6
|
+
require_relative 'platform'
|
7
|
+
require_relative 'provision'
|
8
|
+
require_relative 'test_data'
|
9
|
+
require_relative 'utils'
|
10
|
+
require_relative 'version'
|
11
|
+
require_relative 'test_runner/log_formatter'
|
12
|
+
|
3
13
|
module CemAcpt
|
4
14
|
# Namespace for all Runner-related classes and modules
|
5
15
|
module TestRunner
|
6
|
-
|
7
|
-
|
8
|
-
|
16
|
+
# Holds all the Runner related code
|
17
|
+
class Runner
|
18
|
+
include CemAcpt::Logging
|
19
|
+
|
20
|
+
attr_reader :duration, :exit_code
|
21
|
+
|
22
|
+
def initialize(config)
|
23
|
+
@config = config
|
24
|
+
@run_data = {}
|
25
|
+
@duration = 0
|
26
|
+
@exit_code = 0
|
27
|
+
@results = nil
|
28
|
+
@http_statuses = []
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)}>"
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
@run_data = {}
|
41
|
+
@start_time = Time.now
|
42
|
+
logger.info('CemAcpt') { "Starting CemAcpt v#{CemAcpt::VERSION}..." }
|
43
|
+
logger.info('CemAcpt') { "Test suite started at #{@start_time}..." }
|
44
|
+
logger.info('CemAcpt') { "Using module directory: #{config.get('module_dir')}..." }
|
45
|
+
Dir.chdir(config.get('module_dir')) do
|
46
|
+
keep_terminal_alive
|
47
|
+
@run_data[:priv_key], @run_data[:pub_key], @run_data[:known_hosts] = new_ephemeral_ssh_keys
|
48
|
+
logger.info('CemAcpt') { 'Created ephemeral SSH key pair...' }
|
49
|
+
@run_data[:module_package_path] = build_module_package
|
50
|
+
logger.info('CemAcpt') { "Created module package: #{@run_data[:module_package_path]}..." }
|
51
|
+
@run_data[:test_data] = new_test_data
|
52
|
+
logger.info('CemAcpt') { 'Created test data...' }
|
53
|
+
logger.verbose('CemAcpt') { "Test data: #{@run_data[:test_data]}" }
|
54
|
+
@run_data[:nodes] = new_node_data
|
55
|
+
logger.info('CemAcpt') { 'Created node data...' }
|
56
|
+
logger.verbose('CemAcpt') { "Node data: #{@run_data[:nodes]}" }
|
57
|
+
@instance_names_ips = provision_test_nodes
|
58
|
+
logger.info('CemAcpt') { 'Provisioned test nodes...' }
|
59
|
+
logger.debug('CemAcpt') { "Instance names and IPs: #{@instance_names_ips}" }
|
60
|
+
@results = run_tests(@instance_names_ips.map { |_, v| v['ip'] },
|
61
|
+
config.get('actions.only'),
|
62
|
+
config.get('actions.except'))
|
63
|
+
end
|
64
|
+
ensure
|
65
|
+
clean_up
|
66
|
+
process_test_results
|
67
|
+
end
|
68
|
+
|
69
|
+
def clean_up(trap_context = false)
|
70
|
+
unless trap_context
|
71
|
+
kill_keep_terminal_alive
|
72
|
+
end
|
73
|
+
clean_ephemeral_ssh_keys
|
74
|
+
destroy_test_nodes
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_reader :config
|
80
|
+
|
81
|
+
# @return [Thread] The thread that keeps the terminal alive
|
82
|
+
def keep_terminal_alive
|
83
|
+
return unless config.ci?
|
84
|
+
|
85
|
+
@keep_terminal_alive ||= CemAcpt::Utils::Terminal.keep_terminal_alive
|
86
|
+
end
|
87
|
+
|
88
|
+
def kill_keep_terminal_alive
|
89
|
+
return if @trap_context
|
90
|
+
|
91
|
+
keep_terminal_alive&.kill
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [String] The path to the module package
|
95
|
+
def build_module_package
|
96
|
+
CemAcpt::Utils::Puppet.build_module_package(config.get('module_dir'))
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Array<String>] The paths to the ssh private key, public key, and known hosts file
|
100
|
+
def new_ephemeral_ssh_keys
|
101
|
+
return [nil, nil, nil] if config.get('no_ephemeral_ssh_key')
|
102
|
+
|
103
|
+
CemAcpt::Utils::SSH::Ephemeral.create
|
104
|
+
end
|
105
|
+
|
106
|
+
def clean_ephemeral_ssh_keys
|
107
|
+
return if config.get('no_ephemeral_ssh_key')
|
108
|
+
|
109
|
+
CemAcpt::Utils::SSH::Ephemeral.clean
|
110
|
+
end
|
111
|
+
|
112
|
+
def new_test_data
|
113
|
+
CemAcpt::TestData.acceptance_test_data(config)
|
114
|
+
end
|
115
|
+
|
116
|
+
def new_node_data
|
117
|
+
CemAcpt::Platform.use(config.get('platform.name'), config, @run_data[:test_data])
|
118
|
+
end
|
119
|
+
|
120
|
+
def provision_test_nodes
|
121
|
+
@provisioner = CemAcpt::Provision.new_provisioner(config, @run_data)
|
122
|
+
@provisioner.provision
|
123
|
+
end
|
124
|
+
|
125
|
+
def destroy_test_nodes
|
126
|
+
@provisioner&.destroy
|
127
|
+
end
|
128
|
+
|
129
|
+
def run_tests(hosts, only_actions, except_actions)
|
130
|
+
only_actions = [] if only_actions.nil?
|
131
|
+
except_actions = [] if except_actions.nil?
|
132
|
+
CemAcpt::Goss::Api.run_actions_async(hosts, only: only_actions, except: except_actions)
|
133
|
+
end
|
134
|
+
|
135
|
+
def result_log_formatter
|
136
|
+
@result_log_formatter ||= LogFormatter::GossActionResponse.new(config, @instance_names_ips)
|
137
|
+
end
|
138
|
+
|
139
|
+
def process_test_results
|
140
|
+
if @results.nil?
|
141
|
+
logger.error('CemAcpt') { 'No test results to process' }
|
142
|
+
@exit_code = 1
|
143
|
+
else
|
144
|
+
until @results.empty?
|
145
|
+
result = @results.pop
|
146
|
+
@http_statuses << result.http_status
|
147
|
+
log_test_result(result)
|
148
|
+
end
|
149
|
+
if @http_statuses.empty?
|
150
|
+
logger.error('CemAcpt') { 'No test results to process' }
|
151
|
+
@exit_code = 1
|
152
|
+
else
|
153
|
+
@exit_code = @http_statuses.any? { |s| s.to_i != 200 } ? 1 : 0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
@duration = Time.now - @start_time
|
157
|
+
logger.info('CemAcpt') { "Test suite finished after ~#{duration.round} seconds." }
|
158
|
+
end
|
159
|
+
|
160
|
+
def log_test_result(result)
|
161
|
+
logger.with_ci_group("Test results for #{result_log_formatter.test_name(result)}") do
|
162
|
+
logger.info(result_log_formatter.summary(result))
|
163
|
+
formatted_results = result_log_formatter.results(result)
|
164
|
+
formatted_results.each do |r|
|
165
|
+
if r.start_with?('Passed:')
|
166
|
+
logger.verbose { r }
|
167
|
+
elsif r.start_with?('Skipped:')
|
168
|
+
logger.info { r }
|
169
|
+
else
|
170
|
+
logger.error { r }
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
9
176
|
end
|
10
177
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puppet/modulebuilder'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module CemAcpt
|
7
|
+
module Utils
|
8
|
+
# Puppet-related utilities
|
9
|
+
module Puppet
|
10
|
+
# Builds a Puppet module package.
|
11
|
+
# @param module_dir [String] Path to the module directory. If target_dir
|
12
|
+
# is specified as a relative path, it will be relative to the module dir.
|
13
|
+
# @param target_dir [String] Path to the target directory where the package
|
14
|
+
# will be built. This defaults to the relative path 'pkg/'.
|
15
|
+
# @param should_log [Boolean] Whether or not to log the build process.
|
16
|
+
# @return [String] Path to the built package.
|
17
|
+
def self.build_module_package(module_dir, target_dir = nil, should_log: false)
|
18
|
+
builder_logger = should_log ? logger : nil
|
19
|
+
builder = ::Puppet::Modulebuilder::Builder.new(::File.expand_path(module_dir), target_dir, builder_logger)
|
20
|
+
|
21
|
+
# Validates module metadata by raising exception if invalid
|
22
|
+
_metadata = builder.metadata
|
23
|
+
|
24
|
+
# Builds the module package
|
25
|
+
builder.build
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require_relative '../logging'
|
5
|
+
|
6
|
+
module CemAcpt
|
7
|
+
module Utils
|
8
|
+
# SSH-related utilities
|
9
|
+
module SSH
|
10
|
+
# Class for generating SSH keys
|
11
|
+
class Keygen
|
12
|
+
include CemAcpt::Logging
|
13
|
+
|
14
|
+
DEFAULT_TYPE = 'ed25519'
|
15
|
+
DEFAULT_PASSPHRASE = ''
|
16
|
+
DEFAULT_ROUNDS = 100
|
17
|
+
DEFAULT_BITS = 4096
|
18
|
+
|
19
|
+
def initialize(key_dir: ::File.join(ENV['HOME'], '.ssh'))
|
20
|
+
@key_dir = key_dir
|
21
|
+
@bin_path = find_bin_path
|
22
|
+
end
|
23
|
+
|
24
|
+
def exist?(key_name)
|
25
|
+
key_paths(key_name, chmod: false)
|
26
|
+
true
|
27
|
+
rescue StandardError
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def create(key_name, **options)
|
32
|
+
delete(key_name) # Delete existing keys with same name
|
33
|
+
cmd = new_keygen_cmd(key_name, **options)
|
34
|
+
logger.debug("Creating SSH key with command: #{cmd}")
|
35
|
+
_stdout, stderr, status = Open3.capture3(cmd)
|
36
|
+
raise "Failed to create SSH key! #{stderr}" unless status.success?
|
37
|
+
|
38
|
+
key_paths(key_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(key_name)
|
42
|
+
priv_key = key_path(key_name)
|
43
|
+
pub_key = key_path(key_name, public_key: true)
|
44
|
+
if ::File.file?(priv_key)
|
45
|
+
logger.debug("Deleting private key: #{priv_key}")
|
46
|
+
::File.delete(priv_key)
|
47
|
+
end
|
48
|
+
if ::File.file?(pub_key)
|
49
|
+
logger.debug("Deleting public key: #{pub_key}")
|
50
|
+
::File.delete(pub_key)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
FIND_BIN_PATH_COMMANDS = [
|
57
|
+
"#{ENV['SHELL']} -c 'command -v ssh-keygen'",
|
58
|
+
"#{ENV['SHELL']} -c 'which ssh-keygen'",
|
59
|
+
].freeze
|
60
|
+
|
61
|
+
def find_bin_path(find_cmd = FIND_BIN_PATH_COMMANDS.first)
|
62
|
+
bin_path, stderr, status = Open3.capture3(find_cmd)
|
63
|
+
raise "Cannot find ssh-keygen with command #{find_cmd}: #{stderr}" unless status.success?
|
64
|
+
|
65
|
+
bin_path.chomp
|
66
|
+
rescue StandardError => e
|
67
|
+
return find_bin_path(FIND_BIN_PATH_COMMANDS.last) unless FIND_BIN_PATH_COMMANDS.last == find_cmd
|
68
|
+
|
69
|
+
raise e
|
70
|
+
end
|
71
|
+
|
72
|
+
def new_keygen_cmd(key_name, **options)
|
73
|
+
[
|
74
|
+
@bin_path,
|
75
|
+
'-o',
|
76
|
+
"-t #{options[:type] || DEFAULT_TYPE}",
|
77
|
+
"-C '#{options[:comment] || "cem_acpt_#{key_name}"}'",
|
78
|
+
"-f #{::File.join(@key_dir, key_name)}",
|
79
|
+
"-N '#{options[:passphrase] || DEFAULT_PASSPHRASE}'",
|
80
|
+
"-a #{options[:rounds] || DEFAULT_ROUNDS}",
|
81
|
+
"-b #{options[:bits] || DEFAULT_BITS}",
|
82
|
+
].join(' ')
|
83
|
+
end
|
84
|
+
|
85
|
+
def key_path(file_name, public_key: false)
|
86
|
+
key = ::File.join(@key_dir, file_name)
|
87
|
+
public_key ? "#{key}.pub" : key
|
88
|
+
end
|
89
|
+
|
90
|
+
def key_paths(file_name, chmod: true)
|
91
|
+
priv_key = key_path(file_name)
|
92
|
+
pub_key = key_path(file_name, public_key: true)
|
93
|
+
raise "Private key file #{priv_key} does not exist" unless ::File.file?(priv_key)
|
94
|
+
raise "Public key file #{pub_key} does not exist" unless ::File.file?(pub_key)
|
95
|
+
|
96
|
+
::File.chmod(0o600, priv_key) if chmod
|
97
|
+
::File.chmod(0o600, pub_key) if chmod
|
98
|
+
[priv_key, pub_key]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.ssh_keygen
|
103
|
+
bin_path = `#{ENV['SHELL']} -c 'command -v ssh-keygen'`.chomp
|
104
|
+
raise 'Cannot find ssh-keygen! Install it and verify PATH' unless bin_path
|
105
|
+
|
106
|
+
bin_path
|
107
|
+
rescue StandardError => e
|
108
|
+
raise "Cannot find ssh-keygen! Install it and verify PATH. Orignal error: #{e}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.default_keydir
|
112
|
+
ssh_dir = ::File.join(ENV['HOME'], '.ssh')
|
113
|
+
raise "SSH directory at #{ssh_dir} does not exist" unless ::File.directory?(ssh_dir)
|
114
|
+
|
115
|
+
ssh_dir
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.file_path(file_name, keydir: default_keydir)
|
119
|
+
::File.join(keydir, file_name)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Takes a file name (not path) and optional SSH key directory and returns the paths
|
123
|
+
# to the private key and public key based on the file name given.
|
124
|
+
# @param file_name [String] The base name for the keys
|
125
|
+
# @param keydir [String] An optional SSH key directory
|
126
|
+
def self.key_paths(file_name, keydir: default_keydir)
|
127
|
+
[file_path(file_name, keydir: keydir), file_path("#{file_name}.pub", keydir: keydir)]
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.create(key_name, **options)
|
131
|
+
keygen = Keygen.new
|
132
|
+
keys = keygen.create(key_name, **options)
|
133
|
+
keys + ['/dev/null']
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.create_known_hosts(known_hosts, overwrite: true, keydir: default_keydir)
|
137
|
+
return nil unless known_hosts
|
138
|
+
|
139
|
+
kh_file = file_path(known_hosts, keydir: keydir)
|
140
|
+
::File.open(kh_file, 'w') { |f| f.write("\n") } unless ::File.exist?(kh_file) && !overwrite
|
141
|
+
kh_file
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.set_ssh_file_permissions(*files)
|
145
|
+
files.uniq.compact.map { |p| ::File.chmod(0o600, p) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.ephemeral_ssh_key(keydir: default_keydir)
|
149
|
+
CemAcpt::Utils::SSH::Ephemeral.create(keydir: keydir)
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.clean_ephemeral_keys
|
153
|
+
CemAcpt::Utils::SSH::Ephemeral.clean
|
154
|
+
end
|
155
|
+
|
156
|
+
# Ephemeral SSH key generation and cleanup
|
157
|
+
module Ephemeral
|
158
|
+
PRIV_KEY = 'acpt_test_key'
|
159
|
+
CREATE_OPTS = {
|
160
|
+
type: 'ed25519',
|
161
|
+
bits: '4096',
|
162
|
+
rounds: '100',
|
163
|
+
comment: 'Ephemeral for cem_acpt',
|
164
|
+
password: '',
|
165
|
+
known_hosts: 'acpt_known_hosts',
|
166
|
+
overwrite_known_hosts: true,
|
167
|
+
}.freeze
|
168
|
+
|
169
|
+
class << self
|
170
|
+
attr_accessor :ephemeral_keydir
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.create(keydir: CemAcpt::Utils::SSH.default_keydir)
|
174
|
+
return [false, false, false] if ENV['CEM_ACPT_SSH_PRI_KEY'] # Don't create ephemeral keys if this is set
|
175
|
+
|
176
|
+
self.ephemeral_keydir = keydir
|
177
|
+
@priv_key, @pub_key, @known_hosts = CemAcpt::Utils::SSH.create(PRIV_KEY, keydir: ephemeral_keydir, **CREATE_OPTS)
|
178
|
+
[@priv_key, @pub_key, @known_hosts]
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.clean
|
182
|
+
return if ENV['CEM_ACPT_SSH_PRI_KEY']
|
183
|
+
|
184
|
+
[@priv_key, @pub_key, @known_hosts].each_with_object([]) do |f, arr|
|
185
|
+
next unless f
|
186
|
+
|
187
|
+
path = CemAcpt::Utils::SSH.file_path(f, keydir: ephemeral_keydir)
|
188
|
+
if ::File.exist?(path)
|
189
|
+
::File.delete(path)
|
190
|
+
arr << path
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent-ruby'
|
4
|
+
|
5
|
+
module CemAcpt
|
6
|
+
module Utils
|
7
|
+
# Terminal-related utilities
|
8
|
+
module Terminal
|
9
|
+
def self.keep_terminal_alive
|
10
|
+
executor = Concurrent::SingleThreadExecutor.new
|
11
|
+
executor.post do
|
12
|
+
loop do
|
13
|
+
$stdout.print(".\r")
|
14
|
+
sleep(1)
|
15
|
+
$stdout.print("..\r")
|
16
|
+
sleep(1)
|
17
|
+
$stdout.print("...\r")
|
18
|
+
sleep(1)
|
19
|
+
$stdout.print(" \r")
|
20
|
+
sleep(1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
executor
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|