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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +38 -0
  3. data/Gemfile +4 -3
  4. data/Gemfile.lock +85 -56
  5. data/README.md +144 -83
  6. data/cem_acpt.gemspec +8 -7
  7. data/exe/cem_acpt +41 -7
  8. data/lib/cem_acpt/config.rb +345 -0
  9. data/lib/cem_acpt/core_extensions.rb +17 -61
  10. data/lib/cem_acpt/goss/api/action_response.rb +175 -0
  11. data/lib/cem_acpt/goss/api.rb +83 -0
  12. data/lib/cem_acpt/goss.rb +8 -0
  13. data/lib/cem_acpt/image_name_builder.rb +0 -9
  14. data/lib/cem_acpt/logging/formatter.rb +97 -0
  15. data/lib/cem_acpt/logging.rb +168 -142
  16. data/lib/cem_acpt/platform/base.rb +26 -37
  17. data/lib/cem_acpt/platform/gcp.rb +48 -62
  18. data/lib/cem_acpt/platform.rb +30 -28
  19. data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
  20. data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
  21. data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
  22. data/lib/cem_acpt/provision/terraform.rb +193 -0
  23. data/lib/cem_acpt/provision.rb +20 -0
  24. data/lib/cem_acpt/puppet_helpers.rb +0 -1
  25. data/lib/cem_acpt/test_data.rb +23 -13
  26. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
  27. data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
  28. data/lib/cem_acpt/test_runner.rb +170 -3
  29. data/lib/cem_acpt/utils/puppet.rb +29 -0
  30. data/lib/cem_acpt/utils/ssh.rb +197 -0
  31. data/lib/cem_acpt/utils/terminal.rb +27 -0
  32. data/lib/cem_acpt/utils.rb +4 -138
  33. data/lib/cem_acpt/version.rb +1 -1
  34. data/lib/cem_acpt.rb +70 -20
  35. data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
  36. data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
  37. data/lib/terraform/gcp/linux/main.tf +191 -0
  38. data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
  39. data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
  40. data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
  41. data/lib/terraform/gcp/windows/.keep +0 -0
  42. data/sample_config.yaml +22 -21
  43. metadata +88 -56
  44. data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
  45. data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
  46. data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
  47. data/lib/cem_acpt/bootstrap.rb +0 -12
  48. data/lib/cem_acpt/context.rb +0 -153
  49. data/lib/cem_acpt/platform/base/cmd.rb +0 -71
  50. data/lib/cem_acpt/platform/gcp/cmd.rb +0 -345
  51. data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
  52. data/lib/cem_acpt/platform/vmpooler.rb +0 -24
  53. data/lib/cem_acpt/rspec_utils.rb +0 -242
  54. data/lib/cem_acpt/shared_objects.rb +0 -537
  55. data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
  56. data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
  57. data/lib/cem_acpt/test_runner/runner.rb +0 -210
  58. data/lib/cem_acpt/test_runner/runner_result.rb +0 -103
@@ -1,153 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'platform'
4
- require_relative 'utils'
5
- require_relative 'shared_objects'
6
- require_relative 'test_data'
7
- require_relative 'test_runner/run_handler'
8
- require_relative 'logging'
9
-
10
- module CemAcpt
11
- # Context provides the context in which the RunHandler creates and starts Runners.
12
- module Context
13
- class ContextError < StandardError; end
14
-
15
- class << self
16
- include CemAcpt::Logging
17
-
18
- KEY_PATH = File.join([ENV['HOME'], '.ssh', 'acpt_test_key']).freeze
19
- KH_PATH = File.join([ENV['HOME'], '.ssh', 'acpt_test_known_hosts']).freeze
20
-
21
- def log(msg, level = :info)
22
- real_msg = "CONTEXT: #{msg}"
23
- logger.send(level, real_msg)
24
- end
25
-
26
- # Builds the Puppet module package
27
- # @param opts [Hash] config opts
28
- # @return [String] The path to the Puppet module package
29
- def build_module_package(opts = {})
30
- module_dir = opts[:module_dir] || __dir__
31
- pkg_path = CemAcpt::Utils::Puppet.build_module_package(module_dir)
32
- log("Module package built at #{pkg_path}")
33
- pkg_path
34
- end
35
-
36
- # Creates a SSH key and a SSH known hosts file for the acceptance test suite
37
- def new_test_ssh_key
38
- log('Creating ephemeral SSH key and known hosts file for acceptance test suites...')
39
- @ssh_priv_key, @ssh_pub_key = CemAcpt::Utils::SSH.ephemeral_ssh_key
40
- @ssh_known_hosts = CemAcpt::Utils::SSH.acpt_known_hosts
41
- CemAcpt::Utils::SSH.set_ssh_file_permissions(@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts)
42
- log('Successfully created SSH files...')
43
- log("SSH private key: #{@ssh_priv_key}", :debug)
44
- log("SSH public key: #{@ssh_pub_key}", :debug)
45
- log("SSH known hosts: #{@ssh_known_hosts}", :debug)
46
- end
47
-
48
- # Deletes acceptance test suite SSH files
49
- def clean_test_ssh_key
50
- log('Deleting ephemeral ssh keys and acpt_known_hosts if they exist...')
51
- [@ssh_priv_key, @ssh_pub_key, @ssh_known_hosts].map { |f| File.delete(f) if File.exist?(f) }
52
- end
53
-
54
- # Prints a period to the terminal every 5 seconds in a single line to keep the terminal
55
- # alive. This is used when running in CI mode. Does nothing unless the option `:CI` is
56
- # `true`, or the environment variables CI or GITHUB_ACTION are set to a truthy value.
57
- # @param opts [Hash] config opts
58
- def keep_terminal_alive(opts = {})
59
- @keep_terminal_alive = if opts[:CI] || ENV['CI'] || ENV['GITHUB_ACTION']
60
- CemAcpt::Utils::Terminal.keep_terminal_alive
61
- end
62
- end
63
-
64
- def clean_up_test_suite(opts)
65
- @ctx.node_inventory.clear!
66
- @ctx.node_inventory.clean_local_files
67
- clean_test_ssh_key unless opts[:no_ephemeral_ssh_key]
68
- @run_handler&.destroy_test_nodes
69
- @keep_terminal_alive&.kill
70
- end
71
- end
72
-
73
- # Creates a context (CemAcpt::Context::Ctx) object for the RunHandler to create and start Runners.
74
- # Provides the following objects for the Runners: a config object,
75
- # the test data hash, the node inventory, and the local port allocator.
76
- # Additionally, it creates the platform-specific node objects for each
77
- # test suite in the test data. It then calls the provided block with
78
- # the context objects nodes, config, test_data, and node_inventory.
79
- # @param config_opts [Hash] the config options
80
- def self.with(**opts)
81
- @opts = opts
82
- @start_time = Time.now
83
- raise CemAcpt::Context::ContextError, 'CemAcpt::Context.with requires a block' unless block_given?
84
-
85
- config_file = @opts[:config_file] || File.expand_path('./cem_acpt_config.yaml')
86
- logger.info("Running acceptance test suite at #{@start_time}")
87
- logger.debug("Config opts: #{@opts}")
88
- logger.debug("Config file: #{config_file}")
89
- logger.info("Using module directory: #{@opts[:module_dir]}")
90
- keep_terminal_alive(@opts)
91
- Dir.chdir(opts[:module_dir]) do
92
- new_test_ssh_key unless @opts[:no_ephemeral_ssh_key]
93
- pkg_path = build_module_package(@opts)
94
- @ctx = CemAcpt::Context::Ctx.new(opts: @opts, config_file: config_file, module_package_path: pkg_path)
95
- logger.debug("Created Ctx object #{@ctx}")
96
- @run_handler = CemAcpt::TestRunner::RunHandler.new(@ctx)
97
- logger.debug("Created RunHandler object #{@run_handler}")
98
- yield @run_handler
99
- end
100
- @exit_code = @run_handler.exit_code
101
- rescue StandardError => e
102
- logger.fatal("Acceptance test suite encountered an error: #{e.message}")
103
- logger.fatal(e.backtrace.join("\n"))
104
- @exit_code = 1
105
- ensure
106
- clean_up_test_suite(@opts)
107
- total_minutes = ((Time.now - @start_time) / 60).round
108
- logger.info("Test suite finished in ~#{total_minutes} minutes")
109
- @exit_code || 1
110
- end
111
-
112
- # Ctx holds the context objects for the RunHandler to create and start Runners.
113
- class Ctx
114
- attr_reader :config, :test_data, :module_package_path, :node_inventory, :local_port_allocator
115
-
116
- def initialize(opts: nil, config_file: nil, module_package_path: nil)
117
- @config = CemAcpt::Config.new.load(opts: opts, config_file: config_file)
118
- @test_data = CemAcpt::TestData.acceptance_test_data(@config)
119
- @module_package_path = module_package_path
120
- @node_inventory = CemAcpt::NodeInventory.new
121
- @local_port_allocator = CemAcpt::LocalPortAllocator.new
122
- prep_environment
123
- end
124
-
125
- def nodes
126
- return @nodes if defined?(@nodes)
127
-
128
- raise CemAcpt::Error, 'No platform(s) specified' unless @config.has?('platform') || @config.has?('platforms')
129
-
130
- @nodes = nodes_from_platforms
131
- @nodes
132
- end
133
-
134
- private
135
-
136
- def prep_environment
137
- @node_inventory.clean_local_files
138
- end
139
-
140
- def nodes_from_platforms
141
- nodes = {}
142
- if @config.has?('platform')
143
- nodes[@config.get('platform')] = CemAcpt::Platform.use(@config.get('platform'), @config, @test_data, @local_port_allocator)
144
- elsif @config.has?('platforms')
145
- config.get('platforms').each do |platform|
146
- nodes[platform] = CemAcpt::Platform.use(platform, @config, @test_data, @local_port_allocator)
147
- end
148
- end
149
- nodes
150
- end
151
- end
152
- end
153
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CemAcpt::Platform
4
- require_relative File.join(__dir__, '..', '..', 'logging.rb')
5
-
6
- class CmdError < StandardError; end
7
-
8
- # Base class for command providers. Provides an API for subclasses to implement.
9
- class CmdBase
10
- include CemAcpt::Logging
11
-
12
- attr_reader :env
13
-
14
- def initialize(*_args, env: {}, **_kwargs)
15
- @env = env
16
- end
17
-
18
- def local_exec(*_args, **_kwargs)
19
- raise NotImplementedError, '#local_exec must be implemented by a subclass'
20
- end
21
-
22
- def ssh(_instance_name, _command, _ignore_command_errors: false, _opts: {})
23
- raise NotImplementedError, '#ssh must be implemented by a subclass'
24
- end
25
-
26
- def scp_upload(_instance_name, _local, _remote, _scp_opts: {}, _opts: {})
27
- raise NotImplementedError, '#scp_upload must be implemented by a subclass'
28
- end
29
-
30
- def scp_download(_instance_name, _local, _remote, _scp_opts: {}, _opts: {})
31
- raise NotImplementedError, '#scp_download must be implemented by a subclass'
32
- end
33
-
34
- def ssh_ready?(_instance_name, _timeout = 300, _opts: {})
35
- raise NotImplementedError, '#ssh_ready? must be implemented by a subclass'
36
- end
37
-
38
- def apply_manifest(_instance_name, _manifest, _opts: {})
39
- raise NotImplementedError, '#create_manifest_on_node must be implemented by a subclass'
40
- end
41
-
42
- def run_shell(_instance_name, _command, _opts: {})
43
- raise NotImplementedError, '#run_shell must be implemented by a subclass'
44
- end
45
-
46
- def trim_output(output)
47
- if output.include?("\n")
48
- output.split("\n").map(&:strip).reject(&:empty?).join("\n")
49
- else
50
- output.strip
51
- end
52
- end
53
-
54
- def with_timed_retry(timeout = 300)
55
- return unless block_given?
56
-
57
- last_error = nil
58
- start_time = Time.now
59
- while Time.now - start_time < timeout
60
- begin
61
- output = yield
62
- return output
63
- rescue StandardError => e
64
- last_error = e
65
- sleep(10)
66
- end
67
- end
68
- raise last_error
69
- end
70
- end
71
- end
@@ -1,345 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CemAcpt::Platform::Gcp
4
- require 'json'
5
- require 'open3'
6
- require_relative File.join(__dir__, '..', 'base', 'cmd.rb')
7
-
8
- # This class provides methods to run gcloud commands. It allows for default values to be
9
- # set for the project, zone, and user and can also find these values from the local config.
10
- # Additionally, this class provides a way to run SSH commands against GCP VMs using IAP.
11
- class Cmd < CemAcpt::Platform::CmdBase
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
- require 'net/ssh'
15
- require 'net/ssh/proxy/command'
16
-
17
- @project = project unless project.nil?
18
- @zone = zone unless zone.nil?
19
- @default_out_format = out_format
20
- @default_filter = filter
21
- @user_name = user_name
22
- @local_port = local_port
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
- end
27
-
28
- # Returns either the project name passed in during object initialization
29
- # or the project name from the local config.
30
- def project
31
- @project ||= project_from_config&.chomp
32
- end
33
-
34
- # Returns either the zone name passed in during object initialization
35
- # or the zone name from the local config.
36
- def zone
37
- @zone ||= zone_from_config&.chomp
38
- end
39
-
40
- # Returns either the user name passed in during object initialization
41
- # or queries the current active, authenticated user from gcloud and returns the name.
42
- def user_name
43
- @user_name ||= authenticated_user_name
44
- end
45
-
46
- # Returns the format string passed in during object initialization or
47
- # the default format string.
48
- def format(out_format = nil)
49
- out_format&.chomp || @default_out_format
50
- end
51
-
52
- # Returns the filter string passed in during object initialization or
53
- # the default filter string.
54
- def filter(out_filter = nil)
55
- out_filter&.chomp || @default_filter
56
- end
57
-
58
- def local_port
59
- @local_port ||= rand(49_512..65_535)
60
- end
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
-
74
- # Returns a formatted hash of ssh options to be used with Net::SSH.start.
75
- # If you pass in a GCP VM instance name, this method will configure the
76
- # IAP tunnel ProxyCommand to use. If you pass in an opts hash, it will
77
- # merge the options with the default options.
78
- def ssh_opts(instance_name: nil, use_proxy_command: true, opts: {})
79
- base_opts = default_ssh_opts
80
- if use_proxy_command
81
- base_opts[:proxy] = proxy_command(instance_name, port: base_opts[:port])
82
- base_opts[:host_key_alias] = vm_alias(instance_name)
83
- end
84
- base_opts.merge(opts).reject { |_, v| v.nil? }
85
- end
86
-
87
- # Runs `gcloud` commands locally.
88
- def local_exec(command, out_format: 'json', out_filter: nil, project_flag: true)
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)
91
- raise "gcloud command '#{final_command}' failed: #{stderr}" unless status.success?
92
-
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
101
- end
102
- end
103
-
104
- # Stops a GCP VM instance.
105
- def stop_instance(instance_name)
106
- local_exec("compute instances stop #{instance_name}")
107
- end
108
-
109
- # Deletes a GCP VM instance.
110
- def delete_instance(instance_name)
111
- local_exec("compute instances delete #{instance_name} --quiet")
112
- rescue StandardError
113
- # Ignore errors when deleting instances.
114
- end
115
-
116
- # Returns a Hash describing a GCP VM instance.
117
- def vm_describe(instance_name, out_format: 'json', out_filter: nil)
118
- local_exec("compute instances describe #{instance_name}", out_format: out_format, out_filter: out_filter)
119
- end
120
-
121
- # This function runs the specified command as the currently authenticated user
122
- # on the given CGP VM via SSH. Using `ssh` does not invoke the gcloud command
123
- # and is dependent on the system's SSH configuration.
124
- def ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {})
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}'")
128
- result = ssh.exec!(cmd)
129
- if result.exitstatus != 0 && !ignore_command_errors
130
- abridged_cmd = cmd.length > 100 ? "#{cmd[0..100]}..." : cmd
131
- raise "Failed to run SSH command \"#{abridged_cmd}\" on #{instance_name}: #{result}"
132
- end
133
- result
134
- end
135
- end
136
-
137
- # Uploads a file to the specified VM.
138
- def scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {})
139
- raise "Local file #{local} does not exist" unless File.exist?(local)
140
-
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')
150
- end
151
-
152
- # Downloads a file from the specified VM.
153
- def scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {})
154
- raise "Local file #{local} does not exist" unless File.exist?(local)
155
-
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')
165
- end
166
-
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
180
- end
181
-
182
- # This function spawns a background thread to run a GCP IAP tunnel, run the given
183
- # code block, then kill the thread. The code block will be yielded ssh_opts that
184
- # are used to configure SSH connections over the IAP tunnel. The IAP tunnel is
185
- # run in it's own thread and will not block the main thread. The IAP tunnel is
186
- # killed when the code block exits. All SSH connections made in the code block
187
- # must be made to the host '127.0.0.1' using the yielded ssh_opts and not the VM name.
188
- # @param instance_name [String] The name of the GCP VM instance to connect to.
189
- # @param instance_port [Integer] The port to connect to on the GCP VM instance.
190
- # @param disable_connection_check [Boolean] If true, the connection check will be disabled.
191
- def with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false, &block)
192
- cmd = [
193
- 'compute start-iap-tunnel',
194
- "#{instance_name} #{instance_port}",
195
- "--local-host-port=localhost:#{local_port}",
196
- ]
197
- cmd << '--disable-connection-check' if disable_connection_check
198
- tunnel_ssh_opts = {
199
- proxy: nil,
200
- port: local_port,
201
- forward_agent: false,
202
- }
203
- final_cmd = format_gcloud_command(cmd.join(' '), out_format: nil, project_flag: false)
204
- tunnel_pid = Process.spawn(env, final_cmd)
205
- begin
206
- block.call(ssh_opts(use_proxy_command: false, opts: tunnel_ssh_opts))
207
- ensure
208
- Process.kill('TERM', tunnel_pid)
209
- end
210
- end
211
-
212
- def project_from_config
213
- local_exec('config get-value project', out_format: nil, project_flag: false)
214
- end
215
-
216
- def zone_from_config
217
- local_exec('config get-value compute/zone', out_format: nil, project_flag: false)
218
- end
219
-
220
- def run_shell(instance_name, cmd, opts = {})
221
- ssh_opts = opts.key?(:ssh_opts) ? opts[:ssh_opts] : {}
222
- command = [
223
- 'sudo -n -u root -i',
224
- cmd,
225
- ]
226
- ssh(
227
- instance_name,
228
- command.join(' '),
229
- ignore_command_errors: true,
230
- opts: ssh_opts,
231
- )
232
- end
233
-
234
- def apply_manifest(instance_name, manifest, opts = {})
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
239
- end
240
- apply_cmd = [opts[:puppet_path], 'apply', '/tmp/acpt_manifest.pp']
241
- apply_cmd << '--trace' if opts[:apply][:trace]
242
- apply_cmd << "--hiera_config=#{opts[:apply][:hiera_config]}" if opts[:apply][:hiera_config]
243
- apply_cmd << '--debug' if opts[:apply][:debug]
244
- apply_cmd << '--noop' if opts[:apply][:noop]
245
- apply_cmd << '--detailed-exitcodes' if opts[:apply][:detailed_exitcodes]
246
-
247
- run_shell(
248
- instance_name,
249
- apply_cmd.join(' '),
250
- opts
251
- )
252
- end
253
-
254
- private
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
-
286
- def vm_alias(vm)
287
- "compute.#{vm_describe(vm)['id']}"
288
- end
289
-
290
- # Default options for Net::SSH
291
- def default_ssh_opts
292
- {
293
- auth_methods: ['publickey'],
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
300
- non_interactive: true,
301
- port: 22,
302
- user: user_name,
303
- user_known_hosts_file: File.join(ENV['HOME'], '.ssh', 'acpt_test_known_hosts'),
304
- verify_host_key: :never,
305
- }
306
- end
307
-
308
- # Returns a Proxy::Command object to use for the SSH option ProxyCommand.
309
- # This works in the same way that `gcloud compute ssh --tunnel-through-iap` does.
310
- def proxy_command(instance_name, port: 22, quiet: true, verbosity: 'debug')
311
- proxy_command = [
312
- 'gcloud compute start-iap-tunnel',
313
- "#{instance_name} #{port}",
314
- '--listen-on-stdin',
315
- "--project=#{project}",
316
- "--zone=#{zone}",
317
- ]
318
- #proxy_command << '--no-user-output-enabled --quiet' if quiet
319
- proxy_command << "--verbosity=#{verbosity}" unless quiet || verbosity.nil? || verbosity.empty?
320
- Net::SSH::Proxy::Command.new(proxy_command.join(' '))
321
- end
322
-
323
- # This function checks to ensure that the gcloud tool is available on the system.
324
- def gcloud?
325
- ver = local_exec('--version', out_format: nil, project_flag: false)
326
- !ver.nil? && !ver.empty?
327
- rescue RuntimeError, Errno::ENOENT
328
- false
329
- end
330
-
331
- # This function checks to ensure that a user is authenticated with gcloud.
332
- def authenticated?
333
- local_exec('auth list', project_flag: false).any? { |a| a['status'] == 'ACTIVE' }
334
- rescue RuntimeError, Errno::ENOENT
335
- false
336
- end
337
-
338
- # This function returns the currently authenticated user's name sanitized for use in SSH
339
- def authenticated_user_name
340
- local_exec('compute os-login describe-profile', project_flag: false)['posixAccounts'][0]['username']
341
- rescue StandardError => e
342
- raise "failed to find authenticated user name from os-login profile: #{e.message}"
343
- end
344
- end
345
- end