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.
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) ⇒ 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_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, timeout = 300, 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) ⇒ 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_level, #current_log_level, included, #logger, new_log_config, #new_log_config, new_log_formatter, new_log_level, #new_log_level
Constructor Details
#initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: 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) super require 'net/ssh' require 'net/ssh/proxy/command' require 'net/scp' @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 raise 'gcloud command not available' unless gcloud? raise 'gcloud is not authenticated' unless authenticated? end |
Instance Method Details
#apply_manifest(instance_name, manifest, opts = {}) ⇒ Object
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 213 def apply_manifest(instance_name, manifest, opts = {}) require 'tempfile' temp_manifest = Tempfile.new('acpt_manifest') temp_manifest.write(manifest) temp_manifest.close begin scp_upload( instance_name, temp_manifest.path, '/tmp/acpt_manifest.pp', opts: opts[:ssh_opts], ) ensure temp_manifest.unlink end apply_cmd = [ 'sudo -n -u root -i', "#{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] ssh( instance_name, apply_cmd.join(' '), ignore_command_errors: true, opts: opts[:ssh_opts] ) end |
#delete_instance(instance_name) ⇒ Object
Deletes a GCP VM instance.
100 101 102 103 104 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 100 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&.chomp 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&.chomp end |
#local_exec(command, out_format: 'json', out_filter: nil, project_flag: true) ⇒ Object
Runs `gcloud` commands locally.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 76 def local_exec(command, out_format: 'json', out_filter: nil, project_flag: true) cmd_parts = ['gcloud', command] cmd_parts << "--project=#{project.chomp}" unless !project_flag || project.nil? cmd_parts << "--format=#{format(out_format)}" if format(out_format) cmd_parts << "--filter=\"#{filter(out_filter)}\"" if filter(out_filter) final_command = cmd_parts.join(' ') stdout, stderr, status = Open3.capture3(final_command) raise "gcloud command '#{final_command}' failed: #{stderr}" unless status.success? begin return ::JSON.parse(stdout) if format(out_format) == 'json' rescue ::JSON::ParserError return stdout end stdout 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
190 191 192 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 190 def project_from_config local_exec('config get-value project', out_format: nil, project_flag: false) end |
#run_shell(instance_name, cmd, opts = {}) ⇒ Object
198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 198 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, use_proxy_command: false, 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.
136 137 138 139 140 141 142 143 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 136 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) hostname = use_proxy_command ? vm_alias(instance_name) : instance_name Net::SCP.start(hostname, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |scp| scp.download(remote, local, scp_opts).wait end end |
#scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {}) ⇒ Object
Uploads a file to the specified VM.
126 127 128 129 130 131 132 133 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 126 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) hostname = use_proxy_command ? vm_alias(instance_name) : instance_name Net::SCP.start(hostname, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |scp| scp.upload(local, remote, scp_opts).wait end 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.
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 114 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| 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_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.
66 67 68 69 70 71 72 73 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 66 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, timeout = 300, opts: {}) ⇒ Boolean
145 146 147 148 149 150 151 152 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 145 def ssh_ready?(instance_name, timeout = 300, opts: {}) with_timed_retry(timeout) do logger.debug("Testing SSH connection to #{instance_name}") Net::SSH.start(instance_name, user_name, ssh_opts(instance_name: instance_name, opts: opts)) do |_| true end end end |
#stop_instance(instance_name) ⇒ Object
Stops a GCP VM instance.
95 96 97 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 95 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&.chomp end |
#vm_describe(instance_name, out_format: 'json', out_filter: nil) ⇒ Object
Returns a Hash describing a GCP VM instance.
107 108 109 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 107 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) ⇒ 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.
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 163 def with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false) return unless block_given? 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, } begin thread = Thread.new do local_exec(cmd.join(' ')) end yield ssh_opts(use_proxy_command: false, opts: tunnel_ssh_opts) thread.exit rescue IOError # Ignore errors when killing the thread. ensure thread.exit 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
194 195 196 |
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 194 def zone_from_config local_exec('config get-value compute/zone', out_format: nil, project_flag: false) end |