cem_acpt 0.1.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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