cem_acpt 0.1.0 → 0.2.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 +7 -0
- data/Gemfile.lock +36 -15
- data/README.md +8 -4
- data/cem_acpt.gemspec +13 -13
- 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 +121 -81
- 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 +210 -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 +91 -19
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +18 -11
- metadata +50 -47
- data/.travis.yml +0 -6
- data/lib/cem_acpt/runner.rb +0 -304
data/lib/cem_acpt/logging.rb
CHANGED
@@ -5,8 +5,55 @@ require 'logger'
|
|
5
5
|
module CemAcpt
|
6
6
|
# Logging for CemAcpt
|
7
7
|
module Logging
|
8
|
+
LEVEL_MAP = {
|
9
|
+
'debug' => Logger::DEBUG,
|
10
|
+
'info' => Logger::INFO,
|
11
|
+
'warn' => Logger::WARN,
|
12
|
+
'error' => Logger::ERROR,
|
13
|
+
'fatal' => Logger::FATAL,
|
14
|
+
'unknown' => Logger::UNKNOWN,
|
15
|
+
}
|
16
|
+
|
17
|
+
# Delegator class for when you want to log to multiple devices
|
18
|
+
# at the same time, such as STDOUT and a file.
|
19
|
+
# @param loggers [::Logger] one or more instances of Logger
|
20
|
+
class MultiLogger
|
21
|
+
def initialize(*loggers)
|
22
|
+
@loggers = loggers
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(m, *args, &block)
|
26
|
+
if @loggers.all? { |l| l.respond_to?(m) }
|
27
|
+
@loggers.map { |l| l.send(m, *args, &block) }
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to_missing?(m, include_private = false)
|
34
|
+
@loggers.all? { |l| l.respond_to?(m) } || super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
8
38
|
class << self
|
9
|
-
|
39
|
+
def new_logger(*logdevs, **configs)
|
40
|
+
new_configs = current_log_config.merge(configs.reject { |_, v| v.nil? })
|
41
|
+
if new_configs.key?(:level) && !LEVEL_MAP.values.include?(new_configs[:level])
|
42
|
+
new_configs[:level] = LEVEL_MAP[new_configs[:level].downcase]
|
43
|
+
end
|
44
|
+
loggers = logdevs.map do |dev|
|
45
|
+
logdev = dev.is_a?(String) ? $stdout : dev
|
46
|
+
logger = Logger.new(
|
47
|
+
logdev,
|
48
|
+
new_configs[:shift_age],
|
49
|
+
new_configs[:shift_size],
|
50
|
+
**new_configs.reject { |k, _| %i[logdev shift_age shift_size].include?(k) },
|
51
|
+
)
|
52
|
+
logger.reopen(dev) if dev.is_a?(String)
|
53
|
+
logger
|
54
|
+
end
|
55
|
+
@logger = CemAcpt::Logging::MultiLogger.new(*loggers)
|
56
|
+
end
|
10
57
|
|
11
58
|
# Exposes a logger instance. Will either use the currently set logger or
|
12
59
|
# create a new one.
|
@@ -29,22 +76,9 @@ module CemAcpt
|
|
29
76
|
# Shortcut method to set logger.level
|
30
77
|
# @param level [String] the log level to set
|
31
78
|
def new_log_level(level)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
when 'info'
|
36
|
-
@logger.level = Logger::INFO
|
37
|
-
when 'warn'
|
38
|
-
@logger.level = Logger::WARN
|
39
|
-
when 'error'
|
40
|
-
@logger.level = Logger::ERROR
|
41
|
-
when 'fatal'
|
42
|
-
@logger.level = Logger::FATAL
|
43
|
-
when 'unknown'
|
44
|
-
@logger.level = Logger::UNKNOWN
|
45
|
-
else
|
46
|
-
raise "Cannot set log level #{level}: invalid level"
|
47
|
-
end
|
79
|
+
raise ArgumentError, 'Log level not recognized' unless LEVEL_MAP[level.downcase]
|
80
|
+
|
81
|
+
@logger.level = LEVEL_MAP[level.downcase]
|
48
82
|
end
|
49
83
|
|
50
84
|
# Shows the current log format style if set, or the default if not.
|
@@ -64,7 +98,7 @@ module CemAcpt
|
|
64
98
|
@current_log_format = :json
|
65
99
|
proc do |severity, datetime, progname, msg|
|
66
100
|
{
|
67
|
-
timestamp: datetime
|
101
|
+
timestamp: datetime,
|
68
102
|
progname: progname,
|
69
103
|
severity: severity,
|
70
104
|
message: msg,
|
@@ -75,10 +109,22 @@ module CemAcpt
|
|
75
109
|
proc do |severity, _datetime, _progname, msg|
|
76
110
|
"#{severity} - #{msg}\n"
|
77
111
|
end
|
112
|
+
when :github_action
|
113
|
+
@current_log_format = :github_action
|
114
|
+
sev_map = {
|
115
|
+
'DEBUG' => '::debug::',
|
116
|
+
'INFO' => '::notice::',
|
117
|
+
'WARN' => '::warning::',
|
118
|
+
'ERROR' => '::error::',
|
119
|
+
'FATAL' => '::error::',
|
120
|
+
}
|
121
|
+
proc do |severity, _datetime, _progname, msg|
|
122
|
+
"#{sev_map[severity]}{#{msg}}\n"
|
123
|
+
end
|
78
124
|
else
|
79
125
|
@current_log_format = :file
|
80
126
|
proc do |severity, datetime, _progname, msg|
|
81
|
-
"[#{datetime
|
127
|
+
"[#{datetime}] #{severity}: #{msg}\n"
|
82
128
|
end
|
83
129
|
end
|
84
130
|
end
|
@@ -143,6 +189,10 @@ module CemAcpt
|
|
143
189
|
# Provides class method wrappers for logging methods
|
144
190
|
def self.included(base)
|
145
191
|
class << base
|
192
|
+
def new_logger(*logdevs, **configs)
|
193
|
+
CemAcpt::Logging.new_logger(*logdevs, **configs)
|
194
|
+
end
|
195
|
+
|
146
196
|
def logger
|
147
197
|
CemAcpt::Logging.logger
|
148
198
|
end
|
@@ -155,6 +205,14 @@ module CemAcpt
|
|
155
205
|
CemAcpt::Logging.new_log_level(level)
|
156
206
|
end
|
157
207
|
|
208
|
+
def current_log_format
|
209
|
+
CemAcpt::Logging.current_log_format
|
210
|
+
end
|
211
|
+
|
212
|
+
def new_log_formatter(f)
|
213
|
+
CemAcpt::Logging.new_log_formatter(f)
|
214
|
+
end
|
215
|
+
|
158
216
|
def current_log_config
|
159
217
|
CemAcpt::Logging.current_log_config
|
160
218
|
end
|
@@ -165,6 +223,10 @@ module CemAcpt
|
|
165
223
|
end
|
166
224
|
end
|
167
225
|
|
226
|
+
def new_logger(*logdevs, **configs)
|
227
|
+
CemAcpt::Logging.new_logger(*logdevs, **configs)
|
228
|
+
end
|
229
|
+
|
168
230
|
# Exposes the logger instance
|
169
231
|
def logger
|
170
232
|
CemAcpt::Logging.logger
|
@@ -180,6 +242,14 @@ module CemAcpt
|
|
180
242
|
CemAcpt::Logging.new_log_level(level)
|
181
243
|
end
|
182
244
|
|
245
|
+
def current_log_format
|
246
|
+
CemAcpt::Logging.current_log_format
|
247
|
+
end
|
248
|
+
|
249
|
+
def new_log_formatter(f)
|
250
|
+
CemAcpt::Logging.new_log_formatter(f)
|
251
|
+
end
|
252
|
+
|
183
253
|
# Exposes the current log config
|
184
254
|
def current_log_config
|
185
255
|
CemAcpt::Logging.current_log_config
|
@@ -190,4 +260,92 @@ module CemAcpt
|
|
190
260
|
CemAcpt::Logging.new_log_config(logdev: logdev, shift_age: shift_age, shift_size: shift_size, level: level, formatter: formatter, datetime_format: datetime_format)
|
191
261
|
end
|
192
262
|
end
|
263
|
+
|
264
|
+
module LoggingAsync
|
265
|
+
require 'concurrent-ruby'
|
266
|
+
|
267
|
+
class << self
|
268
|
+
def log_write_thread
|
269
|
+
@log_write_thread ||= Concurrent::SingleThreadExecutor.new
|
270
|
+
end
|
271
|
+
|
272
|
+
def async_debug(message, prefix = nil)
|
273
|
+
msg = prefix.nil? || prefix.empty? ? message : "#{prefix} #{message}"
|
274
|
+
CemAcpt::Logging.logger.debug(msg)
|
275
|
+
end
|
276
|
+
|
277
|
+
def async_info(message, prefix = nil)
|
278
|
+
msg = prefix.nil? || prefix.empty? ? message : "#{prefix} #{message}"
|
279
|
+
CemAcpt::Logging.logger.info(msg)
|
280
|
+
end
|
281
|
+
|
282
|
+
def async_warn(message, prefix = nil)
|
283
|
+
msg = prefix.nil? || prefix.empty? ? message : "#{prefix} #{message}"
|
284
|
+
CemAcpt::Logging.logger.warn(msg)
|
285
|
+
end
|
286
|
+
|
287
|
+
def async_error(message, prefix = nil)
|
288
|
+
msg = prefix.nil? || prefix.empty? ? message : "#{prefix} #{message}"
|
289
|
+
CemAcpt::Logging.logger.error(msg)
|
290
|
+
end
|
291
|
+
|
292
|
+
def async_fatal(message, prefix = nil)
|
293
|
+
msg = prefix.nil? || prefix.empty? ? message : "#{prefix} #{message}"
|
294
|
+
CemAcpt::Logging.logger.fatal(msg)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Provides class method wrappers for logging methods
|
299
|
+
def self.included(base)
|
300
|
+
class << base
|
301
|
+
def log_write_thread
|
302
|
+
CemAcpt::LoggingAsync.log_write_thread
|
303
|
+
end
|
304
|
+
|
305
|
+
def async_debug(message, prefix = nil)
|
306
|
+
CemAcpt::LoggingAsync.async_debug(message, prefix)
|
307
|
+
end
|
308
|
+
|
309
|
+
def async_info(message, prefix = nil)
|
310
|
+
CemAcpt::LoggingAsync.async_info(message, prefix)
|
311
|
+
end
|
312
|
+
|
313
|
+
def async_warn(message, prefix = nil)
|
314
|
+
CemAcpt::LoggingAsync.async_warn(message, prefix)
|
315
|
+
end
|
316
|
+
|
317
|
+
def async_error(message, prefix = nil)
|
318
|
+
CemAcpt::LoggingAsync.async_error(message, prefix)
|
319
|
+
end
|
320
|
+
|
321
|
+
def async_fatal(message, prefix = nil)
|
322
|
+
CemAcpt::LoggingAsync.async_fatal(message, prefix)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def log_write_thread
|
328
|
+
CemAcpt::LoggingAsync.log_write_thread
|
329
|
+
end
|
330
|
+
|
331
|
+
def async_debug(message, prefix = nil)
|
332
|
+
CemAcpt::LoggingAsync.async_debug(message, prefix)
|
333
|
+
end
|
334
|
+
|
335
|
+
def async_info(message, prefix = nil)
|
336
|
+
CemAcpt::LoggingAsync.async_info(message, prefix)
|
337
|
+
end
|
338
|
+
|
339
|
+
def async_warn(message, prefix = nil)
|
340
|
+
CemAcpt::LoggingAsync.async_warn(message, prefix)
|
341
|
+
end
|
342
|
+
|
343
|
+
def async_error(message, prefix = nil)
|
344
|
+
CemAcpt::LoggingAsync.async_error(message, prefix)
|
345
|
+
end
|
346
|
+
|
347
|
+
def async_fatal(message, prefix = nil)
|
348
|
+
CemAcpt::LoggingAsync.async_fatal(message, prefix)
|
349
|
+
end
|
350
|
+
end
|
193
351
|
end
|
@@ -3,11 +3,17 @@
|
|
3
3
|
module CemAcpt::Platform
|
4
4
|
require_relative File.join(__dir__, '..', '..', 'logging.rb')
|
5
5
|
|
6
|
+
class CmdError < StandardError; end
|
7
|
+
|
6
8
|
# Base class for command providers. Provides an API for subclasses to implement.
|
7
9
|
class CmdBase
|
8
10
|
include CemAcpt::Logging
|
9
11
|
|
10
|
-
|
12
|
+
attr_reader :env
|
13
|
+
|
14
|
+
def initialize(*_args, env: {}, **_kwargs)
|
15
|
+
@env = env
|
16
|
+
end
|
11
17
|
|
12
18
|
def local_exec(*_args, **_kwargs)
|
13
19
|
raise NotImplementedError, '#local_exec must be implemented by a subclass'
|
@@ -56,7 +62,7 @@ module CemAcpt::Platform
|
|
56
62
|
return output
|
57
63
|
rescue StandardError => e
|
58
64
|
last_error = e
|
59
|
-
sleep(
|
65
|
+
sleep(10)
|
60
66
|
end
|
61
67
|
end
|
62
68
|
raise last_error
|
@@ -1,19 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'json'
|
4
|
-
require 'open3'
|
5
|
-
require_relative File.join(__dir__, '..', 'base', 'cmd.rb')
|
6
|
-
|
7
3
|
module CemAcpt::Platform::Gcp
|
4
|
+
require 'json'
|
5
|
+
require 'open3'
|
6
|
+
require_relative File.join(__dir__, '..', 'base', 'cmd.rb')
|
7
|
+
|
8
8
|
# This class provides methods to run gcloud commands. It allows for default values to be
|
9
9
|
# set for the project, zone, and user and can also find these values from the local config.
|
10
10
|
# Additionally, this class provides a way to run SSH commands against GCP VMs using IAP.
|
11
11
|
class Cmd < CemAcpt::Platform::CmdBase
|
12
|
-
def initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil)
|
13
|
-
super
|
12
|
+
def initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil, ssh_key: nil)
|
13
|
+
super(env: { 'CLOUDSDK_PYTHON_SITEPACKAGES' => '1' })
|
14
14
|
require 'net/ssh'
|
15
15
|
require 'net/ssh/proxy/command'
|
16
|
-
require 'net/scp'
|
17
16
|
|
18
17
|
@project = project unless project.nil?
|
19
18
|
@zone = zone unless zone.nil?
|
@@ -21,8 +20,9 @@ module CemAcpt::Platform::Gcp
|
|
21
20
|
@default_filter = filter
|
22
21
|
@user_name = user_name
|
23
22
|
@local_port = local_port
|
24
|
-
|
25
|
-
raise 'gcloud
|
23
|
+
@ssh_key = ssh_key
|
24
|
+
raise CemAcpt::Platform::CmdError, 'gcloud command not available' unless gcloud?
|
25
|
+
raise CemAcpt::Platform::CmdError, 'gcloud is not authenticated' unless authenticated?
|
26
26
|
end
|
27
27
|
|
28
28
|
# Returns either the project name passed in during object initialization
|
@@ -40,25 +40,37 @@ module CemAcpt::Platform::Gcp
|
|
40
40
|
# Returns either the user name passed in during object initialization
|
41
41
|
# or queries the current active, authenticated user from gcloud and returns the name.
|
42
42
|
def user_name
|
43
|
-
@user_name ||= authenticated_user_name
|
43
|
+
@user_name ||= authenticated_user_name
|
44
44
|
end
|
45
45
|
|
46
46
|
# Returns the format string passed in during object initialization or
|
47
47
|
# the default format string.
|
48
48
|
def format(out_format = nil)
|
49
|
-
out_format&.chomp || @default_out_format
|
49
|
+
out_format&.chomp || @default_out_format
|
50
50
|
end
|
51
51
|
|
52
52
|
# Returns the filter string passed in during object initialization or
|
53
53
|
# the default filter string.
|
54
54
|
def filter(out_filter = nil)
|
55
|
-
out_filter&.chomp || @default_filter
|
55
|
+
out_filter&.chomp || @default_filter
|
56
56
|
end
|
57
57
|
|
58
58
|
def local_port
|
59
59
|
@local_port ||= rand(49_512..65_535)
|
60
60
|
end
|
61
61
|
|
62
|
+
def ssh_key
|
63
|
+
return @ssh_key unless @ssh_key.nil?
|
64
|
+
|
65
|
+
if File.exist?(File.join([ENV['HOME'], '.ssh', 'acpt_test_key']))
|
66
|
+
@ssh_key = File.join([ENV['HOME'], '.ssh', 'acpt_test_key'])
|
67
|
+
else
|
68
|
+
logger.debug("Test SSH key not found at #{File.join([ENV['HOME'], '.ssh', 'acpt_test_key'])}, using default")
|
69
|
+
@ssh_key = File.join([ENV['HOME'], '.ssh', 'google_compute_engine'])
|
70
|
+
end
|
71
|
+
@ssh_key
|
72
|
+
end
|
73
|
+
|
62
74
|
# Returns a formatted hash of ssh options to be used with Net::SSH.start.
|
63
75
|
# If you pass in a GCP VM instance name, this method will configure the
|
64
76
|
# IAP tunnel ProxyCommand to use. If you pass in an opts hash, it will
|
@@ -74,21 +86,19 @@ module CemAcpt::Platform::Gcp
|
|
74
86
|
|
75
87
|
# Runs `gcloud` commands locally.
|
76
88
|
def local_exec(command, out_format: 'json', out_filter: nil, project_flag: true)
|
77
|
-
|
78
|
-
|
79
|
-
cmd_parts << "--format=#{format(out_format)}" if format(out_format)
|
80
|
-
cmd_parts << "--filter=\"#{filter(out_filter)}\"" if filter(out_filter)
|
81
|
-
final_command = cmd_parts.join(' ')
|
82
|
-
stdout, stderr, status = Open3.capture3(final_command)
|
89
|
+
final_command = format_gcloud_command(command, out_format: out_format, out_filter: out_filter, project_flag: project_flag)
|
90
|
+
stdout, stderr, status = Open3.capture3(env, final_command)
|
83
91
|
raise "gcloud command '#{final_command}' failed: #{stderr}" unless status.success?
|
84
92
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
93
|
+
if format(out_format) == 'json'
|
94
|
+
begin
|
95
|
+
::JSON.parse(stdout)
|
96
|
+
rescue ::JSON::ParserError
|
97
|
+
stdout.chomp
|
98
|
+
end
|
99
|
+
else
|
100
|
+
stdout.chomp
|
89
101
|
end
|
90
|
-
|
91
|
-
stdout
|
92
102
|
end
|
93
103
|
|
94
104
|
# Stops a GCP VM instance.
|
@@ -112,7 +122,9 @@ module CemAcpt::Platform::Gcp
|
|
112
122
|
# on the given CGP VM via SSH. Using `ssh` does not invoke the gcloud command
|
113
123
|
# and is dependent on the system's SSH configuration.
|
114
124
|
def ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {})
|
115
|
-
Net::SSH.start(instance_name, user_name,
|
125
|
+
Net::SSH.start(instance_name, user_name,
|
126
|
+
ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |ssh|
|
127
|
+
logger.debug("Running SSH command '#{cmd}' on instance '#{instance_name}'")
|
116
128
|
result = ssh.exec!(cmd)
|
117
129
|
if result.exitstatus != 0 && !ignore_command_errors
|
118
130
|
abridged_cmd = cmd.length > 100 ? "#{cmd[0..100]}..." : cmd
|
@@ -126,29 +138,45 @@ module CemAcpt::Platform::Gcp
|
|
126
138
|
def scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {})
|
127
139
|
raise "Local file #{local} does not exist" unless File.exist?(local)
|
128
140
|
|
129
|
-
|
130
|
-
|
131
|
-
scp
|
132
|
-
|
141
|
+
cmd = [
|
142
|
+
'compute',
|
143
|
+
'scp',
|
144
|
+
]
|
145
|
+
cmd << '--strict-host-key-checking=no'
|
146
|
+
cmd << "--tunnel-through-iap" if use_proxy_command
|
147
|
+
cmd << "--recurse" if scp_opts[:recurse]
|
148
|
+
cmd << "#{local} #{instance_name}:#{remote}"
|
149
|
+
local_exec(cmd.join(' '), out_format: 'json')
|
133
150
|
end
|
134
151
|
|
135
152
|
# Downloads a file from the specified VM.
|
136
153
|
def scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {})
|
137
154
|
raise "Local file #{local} does not exist" unless File.exist?(local)
|
138
155
|
|
139
|
-
|
140
|
-
|
141
|
-
scp
|
142
|
-
|
156
|
+
cmd = [
|
157
|
+
'compute',
|
158
|
+
'scp',
|
159
|
+
]
|
160
|
+
cmd << '--strict-host-key-checking=no'
|
161
|
+
cmd << '--tunnel-through-iap' if use_proxy_command
|
162
|
+
cmd << '--recurse' if scp_opts[:recurse]
|
163
|
+
cmd << "#{instance_name}:#{remote} #{local}"
|
164
|
+
local_exec(cmd.join(' '), out_format: 'json')
|
143
165
|
end
|
144
166
|
|
145
|
-
def ssh_ready?(instance_name,
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
167
|
+
def ssh_ready?(instance_name, opts: {})
|
168
|
+
ssh_options = ssh_opts(instance_name: instance_name, opts: opts)
|
169
|
+
logger.debug("Testing SSH connection to #{instance_name} with options #{ssh_options}")
|
170
|
+
gcloud_ssh(instance_name, 'echo "SSH is ready"')
|
171
|
+
logger.debug('Removing ecdsa & ed25519 host keys from config due to bug in jruby-openssl')
|
172
|
+
gcloud_ssh(instance_name, 'sudo sed -E -i "s;HostKey /etc/ssh/ssh_host_(ecdsa|ed25519)_key;;g" /etc/ssh/sshd_config')
|
173
|
+
logger.debug('Restarting SSH service')
|
174
|
+
gcloud_ssh(instance_name, 'sudo systemctl restart sshd', ignore_command_errors: true)
|
175
|
+
logger.info("SSH connection to #{instance_name} is ready")
|
176
|
+
true
|
177
|
+
rescue StandardError => e
|
178
|
+
logger.debug("SSH connection to #{instance_name} failed: #{e}")
|
179
|
+
false
|
152
180
|
end
|
153
181
|
|
154
182
|
# This function spawns a background thread to run a GCP IAP tunnel, run the given
|
@@ -160,9 +188,7 @@ module CemAcpt::Platform::Gcp
|
|
160
188
|
# @param instance_name [String] The name of the GCP VM instance to connect to.
|
161
189
|
# @param instance_port [Integer] The port to connect to on the GCP VM instance.
|
162
190
|
# @param disable_connection_check [Boolean] If true, the connection check will be disabled.
|
163
|
-
def with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false)
|
164
|
-
return unless block_given?
|
165
|
-
|
191
|
+
def with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false, &block)
|
166
192
|
cmd = [
|
167
193
|
'compute start-iap-tunnel',
|
168
194
|
"#{instance_name} #{instance_port}",
|
@@ -174,16 +200,12 @@ module CemAcpt::Platform::Gcp
|
|
174
200
|
port: local_port,
|
175
201
|
forward_agent: false,
|
176
202
|
}
|
203
|
+
final_cmd = format_gcloud_command(cmd.join(' '), out_format: nil, project_flag: false)
|
204
|
+
tunnel_pid = Process.spawn(env, final_cmd)
|
177
205
|
begin
|
178
|
-
|
179
|
-
local_exec(cmd.join(' '))
|
180
|
-
end
|
181
|
-
yield ssh_opts(use_proxy_command: false, opts: tunnel_ssh_opts)
|
182
|
-
thread.exit
|
183
|
-
rescue IOError
|
184
|
-
# Ignore errors when killing the thread.
|
206
|
+
block.call(ssh_opts(use_proxy_command: false, opts: tunnel_ssh_opts))
|
185
207
|
ensure
|
186
|
-
|
208
|
+
Process.kill('TERM', tunnel_pid)
|
187
209
|
end
|
188
210
|
end
|
189
211
|
|
@@ -205,47 +227,62 @@ module CemAcpt::Platform::Gcp
|
|
205
227
|
instance_name,
|
206
228
|
command.join(' '),
|
207
229
|
ignore_command_errors: true,
|
208
|
-
use_proxy_command: false,
|
209
230
|
opts: ssh_opts,
|
210
231
|
)
|
211
232
|
end
|
212
233
|
|
213
234
|
def apply_manifest(instance_name, manifest, opts = {})
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
temp_manifest.close
|
219
|
-
begin
|
220
|
-
scp_upload(
|
221
|
-
instance_name,
|
222
|
-
temp_manifest.path,
|
223
|
-
'/tmp/acpt_manifest.pp',
|
224
|
-
opts: opts[:ssh_opts],
|
225
|
-
)
|
226
|
-
ensure
|
227
|
-
temp_manifest.unlink
|
235
|
+
unless opts[:apply][:no_upload]
|
236
|
+
with_temp_manifest_file(manifest) do |tf|
|
237
|
+
upload_temp_manifest(instance_name, tf.path, remote_path: '/tmp/acpt_manifest.pp', opts: opts)
|
238
|
+
end
|
228
239
|
end
|
229
|
-
apply_cmd = [
|
230
|
-
'sudo -n -u root -i',
|
231
|
-
"#{opts[:puppet_path]} apply /tmp/acpt_manifest.pp"
|
232
|
-
]
|
240
|
+
apply_cmd = [opts[:puppet_path], 'apply', '/tmp/acpt_manifest.pp']
|
233
241
|
apply_cmd << '--trace' if opts[:apply][:trace]
|
234
242
|
apply_cmd << "--hiera_config=#{opts[:apply][:hiera_config]}" if opts[:apply][:hiera_config]
|
235
243
|
apply_cmd << '--debug' if opts[:apply][:debug]
|
236
244
|
apply_cmd << '--noop' if opts[:apply][:noop]
|
237
245
|
apply_cmd << '--detailed-exitcodes' if opts[:apply][:detailed_exitcodes]
|
238
246
|
|
239
|
-
|
247
|
+
run_shell(
|
240
248
|
instance_name,
|
241
249
|
apply_cmd.join(' '),
|
242
|
-
|
243
|
-
opts: opts[:ssh_opts]
|
250
|
+
opts
|
244
251
|
)
|
245
252
|
end
|
246
253
|
|
247
254
|
private
|
248
255
|
|
256
|
+
def format_gcloud_command(command, out_format: 'json', out_filter: nil, project_flag: true)
|
257
|
+
cmd_parts = ['gcloud', command]
|
258
|
+
cmd_parts << "--project=#{project.chomp}" unless !project_flag || project.nil?
|
259
|
+
cmd_parts << "--format=#{format(out_format)}" if format(out_format)
|
260
|
+
cmd_parts << "--filter=\"#{filter(out_filter)}\"" if filter(out_filter)
|
261
|
+
cmd_parts.join(' ')
|
262
|
+
end
|
263
|
+
|
264
|
+
def with_temp_manifest_file(manifest, file_name: 'acpt_manifest')
|
265
|
+
require 'tempfile'
|
266
|
+
tf = Tempfile.new(file_name)
|
267
|
+
tf.write(manifest)
|
268
|
+
tf.close
|
269
|
+
begin
|
270
|
+
yield tf
|
271
|
+
ensure
|
272
|
+
tf.unlink
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def upload_temp_manifest(instance_name, local_path, remote_path: '/tmp/acpt_manifest.pp', opts: {})
|
277
|
+
scp_upload(instance_name, local_path, remote_path, opts: opts[:ssh_opts]) unless opts[:apply][:no_upload]
|
278
|
+
end
|
279
|
+
|
280
|
+
def gcloud_ssh(instance_name, command, ignore_command_errors: false)
|
281
|
+
local_exec("compute ssh --ssh-key-file #{ssh_key} --tunnel-through-iap #{instance_name} --command='#{command}'")
|
282
|
+
rescue StandardError => e
|
283
|
+
raise e unless ignore_command_errors
|
284
|
+
end
|
285
|
+
|
249
286
|
def vm_alias(vm)
|
250
287
|
"compute.#{vm_describe(vm)['id']}"
|
251
288
|
end
|
@@ -253,21 +290,24 @@ module CemAcpt::Platform::Gcp
|
|
253
290
|
# Default options for Net::SSH
|
254
291
|
def default_ssh_opts
|
255
292
|
{
|
256
|
-
|
257
|
-
keys: [File.join(ENV['HOME'], '.ssh', 'google_compute_engine')],
|
258
|
-
keys_only: true,
|
259
|
-
config: false,
|
293
|
+
auth_methods: ['publickey'],
|
260
294
|
check_host_ip: false,
|
295
|
+
compression: true,
|
296
|
+
config: false,
|
297
|
+
keys: [ssh_key],
|
298
|
+
keys_only: true,
|
299
|
+
kex: ['diffie-hellman-group-exchange-sha256'], # ecdh algos cause jruby to shit the bed
|
261
300
|
non_interactive: true,
|
262
301
|
port: 22,
|
263
302
|
user: user_name,
|
264
|
-
user_known_hosts_file: File.join(ENV['HOME'], '.ssh', '
|
303
|
+
user_known_hosts_file: File.join(ENV['HOME'], '.ssh', 'acpt_test_known_hosts'),
|
304
|
+
verify_host_key: :never,
|
265
305
|
}
|
266
306
|
end
|
267
307
|
|
268
308
|
# Returns a Proxy::Command object to use for the SSH option ProxyCommand.
|
269
309
|
# This works in the same way that `gcloud compute ssh --tunnel-through-iap` does.
|
270
|
-
def proxy_command(instance_name, port: 22, quiet: true, verbosity:
|
310
|
+
def proxy_command(instance_name, port: 22, quiet: true, verbosity: 'debug')
|
271
311
|
proxy_command = [
|
272
312
|
'gcloud compute start-iap-tunnel',
|
273
313
|
"#{instance_name} #{port}",
|
@@ -275,7 +315,7 @@ module CemAcpt::Platform::Gcp
|
|
275
315
|
"--project=#{project}",
|
276
316
|
"--zone=#{zone}",
|
277
317
|
]
|
278
|
-
proxy_command << '--no-user-output-enabled --quiet' if quiet
|
318
|
+
#proxy_command << '--no-user-output-enabled --quiet' if quiet
|
279
319
|
proxy_command << "--verbosity=#{verbosity}" unless quiet || verbosity.nil? || verbosity.empty?
|
280
320
|
Net::SSH::Proxy::Command.new(proxy_command.join(' '))
|
281
321
|
end
|
@@ -307,7 +347,7 @@ module CemAcpt::Platform::Gcp
|
|
307
347
|
|
308
348
|
raise 'failed to find authenticated user name' unless uname
|
309
349
|
|
310
|
-
uname.
|
350
|
+
uname.gsub(/[^a-zA-Z0-9_]/, '_')
|
311
351
|
end
|
312
352
|
end
|
313
353
|
end
|
@@ -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
|