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.
@@ -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
- attr_writer :logger
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
- case level.downcase
33
- when 'debug'
34
- @logger.level = Logger::DEBUG
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.iso8601(3),
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.iso8601(3)}] #{severity}: #{msg}\n"
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
- def initialize(*args, **kwargs); end
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(5)
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
- raise 'gcloud command not available' unless gcloud?
25
- raise 'gcloud is not authenticated' unless authenticated?
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&.chomp
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&.chomp
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&.chomp
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
- cmd_parts = ['gcloud', command]
78
- cmd_parts << "--project=#{project.chomp}" unless !project_flag || project.nil?
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
- begin
86
- return ::JSON.parse(stdout) if format(out_format) == 'json'
87
- rescue ::JSON::ParserError
88
- return stdout
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, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |ssh|
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
- hostname = use_proxy_command ? vm_alias(instance_name) : instance_name
130
- Net::SCP.start(hostname, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |scp|
131
- scp.upload(local, remote, scp_opts).wait
132
- end
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
- hostname = use_proxy_command ? vm_alias(instance_name) : instance_name
140
- Net::SCP.start(hostname, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |scp|
141
- scp.download(remote, local, scp_opts).wait
142
- end
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, timeout = 300, opts: {})
146
- with_timed_retry(timeout) do
147
- logger.debug("Testing SSH connection to #{instance_name}")
148
- Net::SSH.start(instance_name, user_name, ssh_opts(instance_name: instance_name, opts: opts)) do |_|
149
- true
150
- end
151
- end
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
- thread = Thread.new do
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
- thread.exit
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
- require 'tempfile'
215
-
216
- temp_manifest = Tempfile.new('acpt_manifest')
217
- temp_manifest.write(manifest)
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
- ssh(
247
+ run_shell(
240
248
  instance_name,
241
249
  apply_cmd.join(' '),
242
- ignore_command_errors: true,
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
- verify_host_key: :accept_new_or_local_tunnel,
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', 'google_compute_known_hosts'),
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: nil)
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.split(%r{[^a-zA-Z0-9_]}).join('_')
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
- CemAcpt::Platform::Gcp::Cmd.new(out_format: 'json').apply_manifest(instance_name, manifest, opts)
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
- CemAcpt::Platform::Gcp::Cmd.new(out_format: 'json').run_shell(instance_name, cmd, opts)
82
+ command_provider.run_shell(instance_name, cmd, opts)
83
83
  end
84
84
  end
85
85
  end