cem_acpt 0.2.5 → 0.6.1
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/.github/workflows/spec.yml +38 -0
- data/Gemfile +4 -3
- data/Gemfile.lock +85 -56
- data/README.md +144 -83
- data/cem_acpt.gemspec +8 -7
- data/exe/cem_acpt +41 -7
- data/lib/cem_acpt/config.rb +345 -0
- data/lib/cem_acpt/core_extensions.rb +17 -61
- data/lib/cem_acpt/goss/api/action_response.rb +175 -0
- data/lib/cem_acpt/goss/api.rb +83 -0
- data/lib/cem_acpt/goss.rb +8 -0
- data/lib/cem_acpt/image_name_builder.rb +0 -9
- data/lib/cem_acpt/logging/formatter.rb +97 -0
- data/lib/cem_acpt/logging.rb +168 -142
- data/lib/cem_acpt/platform/base.rb +26 -37
- data/lib/cem_acpt/platform/gcp.rb +48 -62
- data/lib/cem_acpt/platform.rb +30 -28
- data/lib/cem_acpt/provision/terraform/linux.rb +47 -0
- data/lib/cem_acpt/provision/terraform/os_data.rb +72 -0
- data/lib/cem_acpt/provision/terraform/windows.rb +22 -0
- data/lib/cem_acpt/provision/terraform.rb +193 -0
- data/lib/cem_acpt/provision.rb +20 -0
- data/lib/cem_acpt/puppet_helpers.rb +0 -1
- data/lib/cem_acpt/test_data.rb +23 -13
- data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +104 -0
- data/lib/cem_acpt/test_runner/log_formatter.rb +10 -0
- data/lib/cem_acpt/test_runner.rb +170 -3
- data/lib/cem_acpt/utils/puppet.rb +29 -0
- data/lib/cem_acpt/utils/ssh.rb +197 -0
- data/lib/cem_acpt/utils/terminal.rb +27 -0
- data/lib/cem_acpt/utils.rb +4 -138
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +70 -20
- data/lib/terraform/gcp/linux/goss/puppet_idempotent.yaml +10 -0
- data/lib/terraform/gcp/linux/goss/puppet_noop.yaml +12 -0
- data/lib/terraform/gcp/linux/main.tf +191 -0
- data/lib/terraform/gcp/linux/systemd/goss-acpt.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-idempotent.service +8 -0
- data/lib/terraform/gcp/linux/systemd/goss-noop.service +8 -0
- data/lib/terraform/gcp/windows/.keep +0 -0
- data/sample_config.yaml +22 -21
- metadata +88 -56
- data/lib/cem_acpt/bootstrap/bootstrapper.rb +0 -206
- data/lib/cem_acpt/bootstrap/operating_system/rhel_family.rb +0 -129
- data/lib/cem_acpt/bootstrap/operating_system.rb +0 -17
- data/lib/cem_acpt/bootstrap.rb +0 -12
- data/lib/cem_acpt/context.rb +0 -153
- data/lib/cem_acpt/platform/base/cmd.rb +0 -71
- data/lib/cem_acpt/platform/gcp/cmd.rb +0 -345
- data/lib/cem_acpt/platform/gcp/compute.rb +0 -332
- data/lib/cem_acpt/platform/vmpooler.rb +0 -24
- data/lib/cem_acpt/rspec_utils.rb +0 -242
- data/lib/cem_acpt/shared_objects.rb +0 -537
- data/lib/cem_acpt/spec_helper_acceptance.rb +0 -184
- data/lib/cem_acpt/test_runner/run_handler.rb +0 -187
- data/lib/cem_acpt/test_runner/runner.rb +0 -210
- data/lib/cem_acpt/test_runner/runner_result.rb +0 -103
data/lib/cem_acpt/context.rb
DELETED
@@ -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
|