Class: CemAcpt::Platform::Gcp::Cmd
- Defined in:
- lib/cem_acpt/platform/gcp/cmd.rb
Overview
This class provides methods to run gcloud commands. It allows for default values to be set for the project, zone, and user and can also find these values from the local config. Additionally, this class provides a way to run SSH commands against GCP VMs using IAP.
Constant Summary
Constants included from Logging
Instance Attribute Summary
Attributes inherited from CmdBase
Instance Method Summary collapse
- #apply_manifest(instance_name, manifest, opts = {}) ⇒ Object
-
#delete_instance(instance_name) ⇒ Object
Deletes a GCP VM instance.
-
#filter(out_filter = nil) ⇒ Object
Returns the filter string passed in during object initialization or the default filter string.
-
#format(out_format = nil) ⇒ Object
Returns the format string passed in during object initialization or the default format string.
-
#initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil, ssh_key: nil) ⇒ Cmd
constructor
A new instance of Cmd.
-
#local_exec(command, out_format: 'json', out_filter: nil, project_flag: true) ⇒ Object
Runs `gcloud` commands locally.
- #local_port ⇒ Object
-
#project ⇒ Object
Returns either the project name passed in during object initialization or the project name from the local config.
- #project_from_config ⇒ Object
- #run_shell(instance_name, cmd, opts = {}) ⇒ Object
-
#scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {}) ⇒ Object
Downloads a file from the specified VM.
-
#scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {}) ⇒ Object
Uploads a file to the specified VM.
-
#ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {}) ⇒ Object
This function runs the specified command as the currently authenticated user on the given CGP VM via SSH.
- #ssh_key ⇒ Object
-
#ssh_opts(instance_name: nil, use_proxy_command: true, opts: {}) ⇒ Object
Returns a formatted hash of ssh options to be used with Net::SSH.start.
- #ssh_ready?(instance_name, opts: {}) ⇒ Boolean
-
#stop_instance(instance_name) ⇒ Object
Stops a GCP VM instance.
-
#user_name ⇒ Object
Returns either the user name passed in during object initialization or queries the current active, authenticated user from gcloud and returns the name.
-
#vm_describe(instance_name, out_format: 'json', out_filter: nil) ⇒ Object
Returns a Hash describing a GCP VM instance.
-
#with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false, &block) ⇒ Object
This function spawns a background thread to run a GCP IAP tunnel, run the given code block, then kill the thread.
-
#zone ⇒ Object
Returns either the zone name passed in during object initialization or the zone name from the local config.
- #zone_from_config ⇒ Object
Methods inherited from CmdBase
#trim_output, #with_timed_retry
Methods included from Logging
current_log_config, #current_log_config, current_log_format, #current_log_format, current_log_level, #current_log_level, included, logger, #logger, new_log_config, #new_log_config, new_log_formatter, #new_log_formatter, new_log_level, #new_log_level, new_logger, #new_logger
Constructor Details
#initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil, ssh_key: nil) ⇒ Cmd
Returns a new instance of Cmd.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 12 def initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil, ssh_key: nil) super(env: { 'CLOUDSDK_PYTHON_SITEPACKAGES' => '1' }) require 'net/ssh' require 'net/ssh/proxy/command' @project = project unless project.nil? @zone = zone unless zone.nil? @default_out_format = out_format @default_filter = filter @user_name = user_name @local_port = local_port @ssh_key = ssh_key raise CemAcpt::Platform::CmdError, 'gcloud command not available' unless gcloud? raise CemAcpt::Platform::CmdError, 'gcloud is not authenticated' unless authenticated? end |
Instance Method Details
#apply_manifest(instance_name, manifest, opts = {}) ⇒ Object
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 234 def apply_manifest(instance_name, manifest, opts = {}) unless opts[:apply][:no_upload] with_temp_manifest_file(manifest) do |tf| upload_temp_manifest(instance_name, tf.path, remote_path: '/tmp/acpt_manifest.pp', opts: opts) end end apply_cmd = [opts[:puppet_path], 'apply', '/tmp/acpt_manifest.pp'] apply_cmd << '--trace' if opts[:apply][:trace] apply_cmd << "--hiera_config=#{opts[:apply][:hiera_config]}" if opts[:apply][:hiera_config] apply_cmd << '--debug' if opts[:apply][:debug] apply_cmd << '--noop' if opts[:apply][:noop] apply_cmd << '--detailed-exitcodes' if opts[:apply][:detailed_exitcodes] run_shell( instance_name, apply_cmd.join(' '), opts ) end |
#delete_instance(instance_name) ⇒ Object
Deletes a GCP VM instance.
110 111 112 113 114 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 110 def delete_instance(instance_name) local_exec("compute instances delete #{instance_name} --quiet") rescue StandardError # Ignore errors when deleting instances. end |
#filter(out_filter = nil) ⇒ Object
Returns the filter string passed in during object initialization or the default filter string.
54 55 56 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 54 def filter(out_filter = nil) out_filter&.chomp || @default_filter end |
#format(out_format = nil) ⇒ Object
Returns the format string passed in during object initialization or the default format string.
48 49 50 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 48 def format(out_format = nil) out_format&.chomp || @default_out_format end |
#local_exec(command, out_format: 'json', out_filter: nil, project_flag: true) ⇒ Object
Runs `gcloud` commands locally.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 88 def local_exec(command, out_format: 'json', out_filter: nil, project_flag: true) final_command = format_gcloud_command(command, out_format: out_format, out_filter: out_filter, project_flag: project_flag) stdout, stderr, status = Open3.capture3(env, final_command) raise "gcloud command '#{final_command}' failed: #{stderr}" unless status.success? if format(out_format) == 'json' begin ::JSON.parse(stdout) rescue ::JSON::ParserError stdout.chomp end else stdout.chomp end end |
#local_port ⇒ Object
58 59 60 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 58 def local_port @local_port ||= rand(49_512..65_535) end |
#project ⇒ Object
Returns either the project name passed in during object initialization or the project name from the local config.
30 31 32 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 30 def project @project ||= project_from_config&.chomp end |
#project_from_config ⇒ Object
212 213 214 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 212 def project_from_config local_exec('config get-value project', out_format: nil, project_flag: false) end |
#run_shell(instance_name, cmd, opts = {}) ⇒ Object
220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 220 def run_shell(instance_name, cmd, opts = {}) ssh_opts = opts.key?(:ssh_opts) ? opts[:ssh_opts] : {} command = [ 'sudo -n -u root -i', cmd, ] ssh( instance_name, command.join(' '), ignore_command_errors: true, opts: ssh_opts, ) end |
#scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {}) ⇒ Object
Downloads a file from the specified VM.
153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 153 def scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {}) raise "Local file #{local} does not exist" unless File.exist?(local) cmd = [ 'compute', 'scp', ] cmd << '--strict-host-key-checking=no' cmd << '--tunnel-through-iap' if use_proxy_command cmd << '--recurse' if scp_opts[:recurse] cmd << "#{instance_name}:#{remote} #{local}" local_exec(cmd.join(' '), out_format: 'json') end |
#scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {}) ⇒ Object
Uploads a file to the specified VM.
138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 138 def scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {}) raise "Local file #{local} does not exist" unless File.exist?(local) cmd = [ 'compute', 'scp', ] cmd << '--strict-host-key-checking=no' cmd << "--tunnel-through-iap" if use_proxy_command cmd << "--recurse" if scp_opts[:recurse] cmd << "#{local} #{instance_name}:#{remote}" local_exec(cmd.join(' '), out_format: 'json') end |
#ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {}) ⇒ Object
This function runs the specified command as the currently authenticated user on the given CGP VM via SSH. Using `ssh` does not invoke the gcloud command and is dependent on the system's SSH configuration.
124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 124 def ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {}) Net::SSH.start(instance_name, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |ssh| logger.debug("Running SSH command '#{cmd}' on instance '#{instance_name}'") result = ssh.exec!(cmd) if result.exitstatus != 0 && !ignore_command_errors abridged_cmd = cmd.length > 100 ? "#{cmd[0..100]}..." : cmd raise "Failed to run SSH command \"#{abridged_cmd}\" on #{instance_name}: #{result}" end result end end |
#ssh_key ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 62 def ssh_key return @ssh_key unless @ssh_key.nil? if File.exist?(File.join([ENV['HOME'], '.ssh', 'acpt_test_key'])) @ssh_key = File.join([ENV['HOME'], '.ssh', 'acpt_test_key']) else logger.debug("Test SSH key not found at #{File.join([ENV['HOME'], '.ssh', 'acpt_test_key'])}, using default") @ssh_key = File.join([ENV['HOME'], '.ssh', 'google_compute_engine']) end @ssh_key end |
#ssh_opts(instance_name: nil, use_proxy_command: true, opts: {}) ⇒ Object
Returns a formatted hash of ssh options to be used with Net::SSH.start. If you pass in a GCP VM instance name, this method will configure the IAP tunnel ProxyCommand to use. If you pass in an opts hash, it will merge the options with the default options.
78 79 80 81 82 83 84 85 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 78 def ssh_opts(instance_name: nil, use_proxy_command: true, opts: {}) base_opts = default_ssh_opts if use_proxy_command base_opts[:proxy] = proxy_command(instance_name, port: base_opts[:port]) base_opts[:host_key_alias] = vm_alias(instance_name) end base_opts.merge(opts).reject { |_, v| v.nil? } end |
#ssh_ready?(instance_name, opts: {}) ⇒ Boolean
167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 167 def ssh_ready?(instance_name, opts: {}) = ssh_opts(instance_name: instance_name, opts: opts) logger.debug("Testing SSH connection to #{instance_name} with options #{}") gcloud_ssh(instance_name, 'echo "SSH is ready"') logger.debug('Removing ecdsa & ed25519 host keys from config due to bug in jruby-openssl') gcloud_ssh(instance_name, 'sudo sed -E -i "s;HostKey /etc/ssh/ssh_host_(ecdsa|ed25519)_key;;g" /etc/ssh/sshd_config') logger.debug('Restarting SSH service') gcloud_ssh(instance_name, 'sudo systemctl restart sshd', ignore_command_errors: true) logger.info("SSH connection to #{instance_name} is ready") true rescue StandardError => e logger.debug("SSH connection to #{instance_name} failed: #{e}") false end |
#stop_instance(instance_name) ⇒ Object
Stops a GCP VM instance.
105 106 107 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 105 def stop_instance(instance_name) local_exec("compute instances stop #{instance_name}") end |
#user_name ⇒ Object
Returns either the user name passed in during object initialization or queries the current active, authenticated user from gcloud and returns the name.
42 43 44 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 42 def user_name @user_name ||= authenticated_user_name end |
#vm_describe(instance_name, out_format: 'json', out_filter: nil) ⇒ Object
Returns a Hash describing a GCP VM instance.
117 118 119 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 117 def vm_describe(instance_name, out_format: 'json', out_filter: nil) local_exec("compute instances describe #{instance_name}", out_format: out_format, out_filter: out_filter) end |
#with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false, &block) ⇒ Object
This function spawns a background thread to run a GCP IAP tunnel, run the given code block, then kill the thread. The code block will be yielded ssh_opts that are used to configure SSH connections over the IAP tunnel. The IAP tunnel is run in it's own thread and will not block the main thread. The IAP tunnel is killed when the code block exits. All SSH connections made in the code block must be made to the host '127.0.0.1' using the yielded ssh_opts and not the VM name.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 191 def with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false, &block) cmd = [ 'compute start-iap-tunnel', "#{instance_name} #{instance_port}", "--local-host-port=localhost:#{local_port}", ] cmd << '--disable-connection-check' if disable_connection_check tunnel_ssh_opts = { proxy: nil, port: local_port, forward_agent: false, } final_cmd = format_gcloud_command(cmd.join(' '), out_format: nil, project_flag: false) tunnel_pid = Process.spawn(env, final_cmd) begin block.call(ssh_opts(use_proxy_command: false, opts: tunnel_ssh_opts)) ensure Process.kill('TERM', tunnel_pid) end end |
#zone ⇒ Object
Returns either the zone name passed in during object initialization or the zone name from the local config.
36 37 38 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 36 def zone @zone ||= zone_from_config&.chomp end |
#zone_from_config ⇒ Object
216 217 218 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 216 def zone_from_config local_exec('config get-value compute/zone', out_format: nil, project_flag: false) end |